-
Notifications
You must be signed in to change notification settings - Fork 261
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
Support Strict CSP with a nonce #482
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome! thank you for contributing!
src/server.js
Outdated
dangerouslySetInnerHTML: { | ||
__html: css | ||
} | ||
}) | ||
}) | ||
} | ||
|
||
export function flushToHTML() { | ||
export function flushToHTML(nonce = undefined) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
= undefined
is redundant, you can remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we make the arguments an object? This will allow us to add more options in the future.
function flushToHTML(options = {}) {
// options.nonce
}
src/lib/stylesheet.js
Outdated
@@ -29,6 +27,11 @@ export default class StyleSheet { | |||
this._tags = [] | |||
this._injected = false | |||
this._rulesCount = 0 | |||
|
|||
const node = | |||
typeof document === 'object' && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here you can use this._isBrowser
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like changing this might break my test 😟 because there isn't any browser testing implemented in this repo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
probably in your test you can do global.window = true
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah no you can pass isBrowser: true
to makeRegistry
https://github.com/zeit/styled-jsx/blob/master/test/stylesheet-registry.js#L8-L12
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@giuseppeg Will this not cause unintended side effects because we are telling the application it is running in a browser and it's not really? If somewhere else in the application it checks isBrowser
and if I set it to true, it might try to execute some other type of browser only script.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is an isolated unit test so it is perfectly fine. If you are talking about src/lib/stylesheet.js
instead that's fine too since you are checking that typeof document === 'object'
is defined i.e. you are in a browser environment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was right about the unintended side effects. It's possible that isBrowser == true
and typeof document === 'undefined'
, which will create an error.
Making the change you requested, allows other tests to break code when it sets isBrowser to true, even when it's not technically in a browser. I feel like this is an issue of it's own.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it should behave the same when isBrowser is true. If there are failing tests this is a good thing and we should look into this issue. In a real browser typeof document === 'object'
will be the same as using this._isBrowser
because the latter is just typeof window !== 'undefined;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want me to break the tests, I will, I just don't have time to fix them myself.
readme.md
Outdated
Strict CSP is supported. You must pass a nonce as a parameter to either `flush(nonce)` or `flushToHTML(nonce)` **and** set a `<meta property="csp-nonce" content="nonce">` tag. | ||
|
||
You should generate a nonce per request. | ||
```jsx |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this can be js
src/lib/stylesheet.js
Outdated
@@ -29,6 +27,11 @@ export default class StyleSheet { | |||
this._tags = [] | |||
this._injected = false | |||
this._rulesCount = 0 | |||
|
|||
const node = | |||
typeof document === 'object' && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah no you can pass isBrowser: true
to makeRegistry
https://github.com/zeit/styled-jsx/blob/master/test/stylesheet-registry.js#L8-L12
src/lib/stylesheet.js
Outdated
@@ -41,7 +44,7 @@ export default class StyleSheet { | |||
this._rulesCount === 0, | |||
'optimizeForSpeed cannot be when rules have already been inserted' | |||
) | |||
this.flush() | |||
this.flush(options.flush) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do you need to pass options.flush
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case that method gets other options eventually.
src/stylesheet-registry.js
Outdated
@@ -94,8 +94,8 @@ export default class StyleSheetRegistry { | |||
this.remove(props) | |||
} | |||
|
|||
flush() { | |||
this._sheet.flush() | |||
flush(options = {}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is this for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case someone is using this method and needs to set the nonce.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it is necessary also StyleSheet#flush
is not using those options https://github.com/zeit/styled-jsx/blob/368a6e4e3e1211e84b6bc4bb4db44dcd426a0bd9/src/lib/stylesheet.js#L192
Can you revert this change?
Thanks for your patience @dav-is! To fix the tests you can create a little helper function to mock function withMock(mockFn, testFn) {
const cleanUp = mockFn()
if (typeof cleanUp !== 'function') {
throw new Error('mockFn should return a cleanup function')
}
testFn()
cleanUp()
}
function withMockDocument() {
const originalDocument = global.document
// We need to stub a document in order to simulate the meta tag
global.document = {
querySelector(query) {
t.is(query, 'meta[property="csp-nonce"]')
return {
getAttribute(attr) {
t.is(attr, 'content')
return 'test-nonce'
}
}
}
}
return () => {
global.document = originalDocument
}
}
withMock(
withMockDocument,
() => {
const registry = makeRegistry()
registry.add({ styleId: '123', css: [cssRule] })
t.is(registry._sheet._nonce, 'test-nonce')
}
) |
@giuseppeg All done :) |
@thealjey sorry for the second ping, could you have a look? 🙏 🙌 |
oops, sure! will do in a couple hours :) |
The API is exactly as I thought it should be (although, this will require the changes to be also made to Next.js to actually pass nonce to |
readme.md
Outdated
const nonce = Buffer.from(nanoid()).toString('base64') //ex: N2M0MDhkN2EtMmRkYi00MTExLWFhM2YtNDhkNTc4NGJhMjA3 | ||
``` | ||
|
||
You must then pass a nonce to either `flush({ nonce })` or `flushToHTML({ nonce })` **and** set a `<meta property="csp-nonce" content={nonce} />` tag. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should replace flush
with flushToReact
@thealjey thank you <3
yep we should fix that |
@dav-is thank you a lot for your contribution!! |
@timneutkens @giuseppeg Do you think we could get a release? |
@giuseppeg Could you please make a new release? |
@dav-is published 3.1.0 sorry for the delay |
The updated documentation states that we should apply registry.styles({ nonce }). Where and how can I do this in a Next.js project (14.+)? |
@xereda you can follow this guide. I believe adding the CSP header to the request headers allows Next.js to automatically use the nonce. Then the client uses the CSP additionally sent in the response headers. I would also verify the |
@dav-is I did all of this. I was even able to apply it to scripts, but it's not working for styles. I have no idea where to implement "registry.styles({ nonce })". |
@xereda you may need to file an issue on the Next.js repo then, I would imagine since Next.js handles styled-jsx for you, it should add the nonce for you too |
Content Security Policy
Strict CSP is supported. You must pass a nonce as a parameter to either
flush(nonce)
orflushToHTML(nonce)
and set a<meta property="csp-nonce" content="nonce">
tag.You should generate a nonce per request.
Your CSP policy must have this same nonce as well.
Content-Security-Policy: default-src 'self'; style-src 'self' 'nonce-N2M0MDhkN2EtMmRkYi00MTExLWFhM2YtNDhkNTc4NGJhMjA3';