Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allows setting nonce on generated style tags #376

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ docs
- [server side rendering](https://github.com/threepointone/glamor/blob/master/docs/server.md)
- [performance tips](https://github.com/threepointone/glamor/blob/master/docs/performance.md)
- [what happens when I call css(...rules)?](https://github.com/threepointone/glamor/blob/master/docs/implementation.md)
- [adding a nonce to the style tags for use with CSP](./docs/nonce.md)

extras
---
Expand Down
59 changes: 59 additions & 0 deletions docs/nonce.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
Using a nonce for Content Security Policy (CSP)
---

If the consuming application wishes to apply a CSP header that is strict then in order to use Glamor, the styles will either need to use the `unsafe-inline` rule or a nonce should be added to the style tags as they are dynamically generated.

To have Glamor add a nonce, the nonce should be assigned to a [Webpack variable](https://webpack.js.org/guides/csp/#examples) called `__webpack_nonce__`.

This must be done before any styles have been generated by Glamor.

As an example, if the server was to render the following:

```HTML
<!-- index.html -->
<html>
<body>
<h1>Test application</h1>
<script>
window.NONCE = 'base64-1234-abcd'
</script>
<script src="path-to-entry-bundle.js"></script>
</body>
</html>
```

Then somewhere very early on the packed bundle should contain an import to a file such as:

```JavaScript
// setup-nonce.js
__webpack_nonce__ = window.NONCE

// index.js
import React from 'react' // no glamor styles here
import { customUtil } from './custom/code' // no glamor styles here
import './setup-nonce'
import GlamorStyledComponent from './components/GlamorStyledComponent'
```

The resulting style tags will look like:

```html
<style
type="text/css"
data-glamor=""
nonce="base64-1234-abcd">
.css-icjsl7,[data-css-icjsl7]{color:blue;}
</style>
```

Meaning that the CSP header can look more like

```
default-src 'self'; style-src 'nonce-base64-1234-abcd';
```

rather than:

```
default-src 'self'; style-src 'self' 'unsafe-inline';
```
5 changes: 5 additions & 0 deletions examples/nonce-set.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
Not an example but support the nonce example which has to import
this file
*/
__webpack_nonce__ = "nonce-value"; // eslint-disable-line
23 changes: 23 additions & 0 deletions examples/nonce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from "react";

// Import the test nonce setup before any glamor code is asked to
// produce any styles.
// https://github.com/styled-components/styled-components/issues/887#issuecomment-376479268
import "./nonce-set";
import { css } from "glamor";

let cls = css({ color: "blue" });

export class App extends React.Component {
render() {
return (
<div className={cls}>
Check the DOM, the style tag should have a nonce.
</div>
);
}

componentDidMount() {
console.log("Nonce on style tag:", document.querySelector("style").nonce);
}
}
112 changes: 60 additions & 52 deletions src/sheet.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import assign from 'object-assign'
/*
/*

high performance StyleSheet for css-in-js systems
high performance StyleSheet for css-in-js systems

- uses multiple style tags behind the scenes for millions of rules
- uses multiple style tags behind the scenes for millions of rules
- uses `insertRule` for appending in production for *much* faster performance
- 'polyfills' on server side
- 'polyfills' on server side


// usage

import StyleSheet from 'glamor/lib/sheet'
let styleSheet = new StyleSheet()

styleSheet.inject()
styleSheet.inject()
- 'injects' the stylesheet into the page (or into memory if on server)

styleSheet.insert('#box { border: 1px solid red; }')
- appends a css rule into the stylesheet
styleSheet.insert('#box { border: 1px solid red; }')
- appends a css rule into the stylesheet

styleSheet.flush()
styleSheet.flush()
- empties the stylesheet of all its contents


Expand All @@ -34,7 +34,7 @@ function sheetForTag(tag) {
return tag.sheet
}

// this weirdness brought to you by firefox
// this weirdness brought to you by firefox
for(let i = 0; i < document.styleSheets.length; i++) {
if(document.styleSheets[i].ownerNode === tag) {
return document.styleSheets[i]
Expand All @@ -46,27 +46,35 @@ const isBrowser = typeof window !== 'undefined'
const isDev = (process.env.NODE_ENV === 'development') || (!process.env.NODE_ENV) //(x => (x === 'development') || !x)(process.env.NODE_ENV)
const isTest = process.env.NODE_ENV === 'test'

const oldIE = (() => {
const oldIE = (() => {
if(isBrowser) {
let div = document.createElement('div')
div.innerHTML = '<!--[if lt IE 10]><i></i><![endif]-->'
return div.getElementsByTagName('i').length === 1
}
}
})()

function getWebpackNonce() {
return typeof __webpack_nonce__ !== 'undefined' ? __webpack_nonce__ : null // eslint-disable-line
}

function makeStyleTag() {
const nonce = getWebpackNonce()
let tag = document.createElement('style')
tag.type = 'text/css'
tag.setAttribute('data-glamor', '')
if (nonce) {
tag.setAttribute('nonce', nonce)
}
tag.appendChild(document.createTextNode(''));
(document.head || document.getElementsByTagName('head')[0]).appendChild(tag)
return tag
}

export function StyleSheet({
speedy = !isDev && !isTest,
maxLength = (isBrowser && oldIE) ? 4000 : 65000
} = {}) {
export function StyleSheet({
speedy = !isDev && !isTest,
maxLength = (isBrowser && oldIE) ? 4000 : 65000
} = {}) {
this.isSpeedy = speedy // the big drawback here is that the css won't be editable in devtools
this.sheet = undefined
this.tags = []
Expand All @@ -76,26 +84,26 @@ export function StyleSheet({

assign(StyleSheet.prototype, {
getSheet() {
return sheetForTag(last(this.tags))
return sheetForTag(last(this.tags))
},
inject() {
if(this.injected) {
throw new Error('already injected stylesheet!')
throw new Error('already injected stylesheet!')
}
if(isBrowser) {
this.tags[0] = makeStyleTag()
}
if(isBrowser) {
this.tags[0] = makeStyleTag()
}
else {
// server side 'polyfill'. just enough behavior to be useful.
this.sheet = {
this.sheet = {
cssRules: [],
insertRule: rule => {
// enough 'spec compliance' to be able to extract the rules later
// in other words, just the cssText field
this.sheet.cssRules.push({ cssText: rule })
// enough 'spec compliance' to be able to extract the rules later
// in other words, just the cssText field
this.sheet.cssRules.push({ cssText: rule })
}
}
}
}
this.injected = true
},
speedy(bool) {
Expand All @@ -105,29 +113,29 @@ assign(StyleSheet.prototype, {
this.isSpeedy = !!bool
},
_insert(rule) {
// this weirdness for perf, and chrome's weird bug
// this weirdness for perf, and chrome's weird bug
// https://stackoverflow.com/questions/20007992/chrome-suddenly-stopped-accepting-insertrule
try {
try {
let sheet = this.getSheet()
sheet.insertRule(rule, rule.indexOf('@import') !== -1 ? 0 : sheet.cssRules.length)
}
catch(e) {
if(isDev) {
// might need beter dx for this
// might need beter dx for this
console.warn('whoops, illegal rule inserted', rule) //eslint-disable-line no-console
}
}
}
}

},
insert(rule) {
insert(rule) {

if(isBrowser) {
// this is the ultrafast version, works across browsers
if(this.isSpeedy && this.getSheet().insertRule) {
// this is the ultrafast version, works across browsers
if(this.isSpeedy && this.getSheet().insertRule) {
this._insert(rule)
}
// more browser weirdness. I don't even know
// else if(this.tags.length > 0 && this.tags::last().styleSheet) {
// more browser weirdness. I don't even know
// else if(this.tags.length > 0 && this.tags::last().styleSheet) {
// this.tags::last().styleSheet.cssText+= rule
// }
else{
Expand All @@ -137,34 +145,34 @@ assign(StyleSheet.prototype, {
} else {
last(this.tags).appendChild(document.createTextNode(rule))
}
}
}
}
else{
// server side is pretty simple
// server side is pretty simple
this.sheet.insertRule(rule, rule.indexOf('@import') !== -1 ? 0 : this.sheet.cssRules.length)
}

this.ctr++
if(isBrowser && this.ctr % this.maxLength === 0) {
this.tags.push(makeStyleTag())
}
return this.ctr -1
},
// commenting this out till we decide on v3's decision
// commenting this out till we decide on v3's decision
// _replace(index, rule) {
// // this weirdness for perf, and chrome's weird bug
// // this weirdness for perf, and chrome's weird bug
// // https://stackoverflow.com/questions/20007992/chrome-suddenly-stopped-accepting-insertrule
// try {
// let sheet = this.getSheet()
// sheet.deleteRule(index) // todo - correct index here
// try {
// let sheet = this.getSheet()
// sheet.deleteRule(index) // todo - correct index here
// sheet.insertRule(rule, index)
// }
// catch(e) {
// if(isDev) {
// // might need beter dx for this
// // might need beter dx for this
// console.warn('whoops, problem replacing rule', rule) //eslint-disable-line no-console
// }
// }
// }
// }

// }
// replace(index, rule) {
Expand All @@ -173,7 +181,7 @@ assign(StyleSheet.prototype, {
// this._replace(index, rule)
// }
// else {
// let _slot = Math.floor((index + this.maxLength) / this.maxLength) - 1
// let _slot = Math.floor((index + this.maxLength) / this.maxLength) - 1
// let _index = (index % this.maxLength) + 1
// let tag = this.tags[_slot]
// tag.replaceChild(document.createTextNode(rule), tag.childNodes[_index])
Expand All @@ -197,19 +205,19 @@ assign(StyleSheet.prototype, {
// todo - look for remnants in document.styleSheets
}
else {
// simpler on server
// simpler on server
this.sheet.cssRules = []
}
this.injected = false
},
},
rules() {
if(!isBrowser) {
return this.sheet.cssRules
}
let arr = []
this.tags.forEach(tag => arr.splice(arr.length, 0, ...Array.from(
sheetForTag(tag).cssRules
)))
sheetForTag(tag).cssRules
)))
return arr
}
})
Loading