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

Support Strict CSP with a nonce #482

Merged
merged 9 commits into from
Sep 5, 2018
Merged

Support Strict CSP with a nonce #482

merged 9 commits into from
Sep 5, 2018

Conversation

dav-is
Copy link
Contributor

@dav-is dav-is commented Aug 28, 2018

Content Security Policy

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.

import nanoid from 'nanoid'

const nonce = Buffer.from(nanoid()).toString('base64') //ex: N2M0MDhkN2EtMmRkYi00MTExLWFhM2YtNDhkNTc4NGJhMjA3

Your CSP policy must have this same nonce as well.
Content-Security-Policy: default-src 'self'; style-src 'self' 'nonce-N2M0MDhkN2EtMmRkYi00MTExLWFhM2YtNDhkNTc4NGJhMjA3';

Copy link
Collaborator

@giuseppeg giuseppeg left a 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) {
Copy link
Collaborator

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.

Copy link
Collaborator

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 
}

@@ -29,6 +27,11 @@ export default class StyleSheet {
this._tags = []
this._injected = false
this._rulesCount = 0

const node =
typeof document === 'object' &&
Copy link
Collaborator

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

Copy link
Contributor Author

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 ☹️

Copy link
Collaborator

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.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

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.

Copy link
Collaborator

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.

Copy link
Contributor Author

@dav-is dav-is Aug 30, 2018

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.

Copy link
Collaborator

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;

Copy link
Contributor Author

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 Show resolved Hide resolved
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be js

@@ -29,6 +27,11 @@ export default class StyleSheet {
this._tags = []
this._injected = false
this._rulesCount = 0

const node =
typeof document === 'object' &&
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test/stylesheet-registry.js Outdated Show resolved Hide resolved
@@ -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)
Copy link
Collaborator

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?

Copy link
Contributor Author

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.

@@ -94,8 +94,8 @@ export default class StyleSheetRegistry {
this.remove(props)
}

flush() {
this._sheet.flush()
flush(options = {}) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this for?

Copy link
Contributor Author

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.

Copy link
Collaborator

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?

@giuseppeg
Copy link
Collaborator

Thanks for your patience @dav-is! To fix the tests you can create a little helper function to mock document and document.querySelector like you did in your test. Something like

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')
  }
)

@dav-is
Copy link
Contributor Author

dav-is commented Aug 31, 2018

@giuseppeg All done :)

@giuseppeg
Copy link
Collaborator

@thealjey do you mind taking a quick look at this PR and let us know if you had something like this in mind when you opened #423 ?

@timneutkens
Copy link
Member

@thealjey sorry for the second ping, could you have a look? 🙏 🙌

@thealjey
Copy link

thealjey commented Sep 5, 2018

oops, sure! will do in a couple hours :)

@thealjey
Copy link

thealjey commented Sep 5, 2018

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 flush and, preferably, create the meta tag also).
I do have a question regarding the actual changes.
The readme states that you can pass nonce to the flush function, but I do not see flush actually accept it (flushToHTML and flushToReact both do, but not flush). It only reads the value from the meta tag on the client.
Maybe I just don't see something.

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.
Copy link
Collaborator

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

@giuseppeg
Copy link
Collaborator

giuseppeg commented Sep 5, 2018

@thealjey thank you <3

The readme states that you can pass nonce to the flush function, but I do not see flush actually accept it

yep we should fix that

@giuseppeg giuseppeg merged commit 03af155 into vercel:master Sep 5, 2018
@giuseppeg
Copy link
Collaborator

@dav-is thank you a lot for your contribution!!

@dav-is
Copy link
Contributor Author

dav-is commented Sep 7, 2018

@timneutkens @giuseppeg Do you think we could get a release?

@dav-is dav-is deleted the csp-nonce branch September 7, 2018 18:49
@dav-is
Copy link
Contributor Author

dav-is commented Sep 24, 2018

@giuseppeg Could you please make a new release?

@giuseppeg
Copy link
Collaborator

@dav-is published 3.1.0 sorry for the delay

@xereda
Copy link

xereda commented Mar 27, 2024

The updated documentation states that we should apply registry.styles({ nonce }). Where and how can I do this in a Next.js project (14.+)?

@dav-is
Copy link
Contributor Author

dav-is commented Mar 27, 2024

@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 <meta property="csp-nonce" content={nonce} /> is returned in the html, if not, you may have to do it manually using the x-nonce request header mentioned in the guide.

@xereda
Copy link

xereda commented Mar 28, 2024

@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 })".

@dav-is
Copy link
Contributor Author

dav-is commented Mar 28, 2024

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants