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

[WIP] Strong CSP Support #4943

Closed
wants to merge 85 commits into from
Closed

[WIP] Strong CSP Support #4943

wants to merge 85 commits into from

Conversation

dav-is
Copy link
Contributor

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

Support for Content Security Policy requires testing Next.js with the strictest policy possible. This is to support the use of a strict policy on hardened Next.js application. To be compatible, Next.js must not use any inline styles or scripts. No dependencies can use eval(). This is all to help prevent XSS attacks.

With recent reports of malicious packages on npm, users can benefit from whitelisting the origins they communicate with avoid leaking private data (unintentionally or maliciously).

TODO

New Configuration Options

// next.config.js
module.exports = {
  //This is the same policy we test against internally
  contentSecurityPolicy: "default-src 'none'; script-src 'self'; style-src 'nonce-{style-nonce}'; connect-src 'self';img-src 'self';"
}

Server Side Rendering State is stored inside a <script type="application/json"> which is not executed so is not considered an inline script.

If using static exporting, we can't set a CSP in a header, so the policy will be added in a meta tag. If using static exporting nonces cannot work, so either 'unsafe-inline' is required for styles or you will have to use a different CSS-in-JS library that can export statically as well. It would be cool if we could output a now.json file to add a CSP header to static hosting.

}} />}
{staticMarkup ? null : <script
id="server-app-state"
type="application/json"
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't you use another tag type instead for transferring the app-state? I suppose even though its type is application/json some browsers may treat it as an inline script.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No. This is an html standard. <script type=“application/json”> isn’t JavaScript and CSP defines script-src as “Specifies valid sources for JavaScript”. This is also the most valid way for SSR to use CSP.

Copy link
Contributor

Choose a reason for hiding this comment

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

I wouldn't be so optimistic about all browsers implementing this correctly. Anyway, alternatively I could think about using a data- attribute on the head element to hold the app-state.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

https://www.drupal.org/node/2513818

If it means anything to you, a quick google search found that Drupal is using this method.

),
}}
/>}
<script
Copy link
Contributor

Choose a reason for hiding this comment

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

This defeats the purpose of this PR, defines an inline script.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As stated above, CSP ignores non-JavaScript <script> tags. Using a <script> tag is the html standard of transmitting JSON as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

But I don't see any type attribute set on this script tag, so it's treated as javascript. Have you been testing your PR with a strict CSP?

Copy link
Contributor Author

@dav-is dav-is Aug 11, 2018

Choose a reason for hiding this comment

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

Oh I just realized which script tag this comment was attached to. This script tag is temporary until I can figure out how to define this through webpack (this is why it’s a WIP)

}}
/>
<script
src={`${assetPrefix}/_next/static/runtime/bootstrap.js`}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd like to see this discussed upfront, since this adds a network request. I'd rather prefer to have the app-state/bootstrap code integrated into some common bundle (_app?).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems like all the pages are loaded asyncronously so they could be loaded before runtime/main.js which is where next is loaded. I added a new file called runtime/bootstrap.js that should be small and won’t be loaded asyncronously and will always run before everything else. This is a potential blocking process, but if we keep the file small and never change it so we can keep a huge cache time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The app-state is data (nothing that is executed) that will change with each request to SSR, but the bootstrap.js can be cached instead of being sent with each html request so it’s actually going to save bytes over time, and if you are using http/2 you can have more requests without any performance degradation.

@dav-is dav-is changed the title WIP: Move inline scripts elsewhere for strong CSP support Move Inline Script to Bootstraping File to Support Strong CSP Aug 28, 2018
@dav-is
Copy link
Contributor Author

dav-is commented Aug 28, 2018

This is ready for review! 😄

CC: @dbo

README.md Outdated

#### Content Security Policy

Next.js *mostly* supports [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP). You can use the following CSP policy to get started: `default-src 'self'; style-src 'self' 'unsafe-inline';`
Copy link
Contributor

Choose a reason for hiding this comment

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

I think unsafe-inline is a bad suggestion for CSP and not needed, since you could define a hash for the inline script (NextScript).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is inline style. Completely different issue. Trust me. <style> blocks are just randomly added and removed from the DOM. It takes some special logic to get that working.

Copy link
Contributor Author

@dav-is dav-is Aug 28, 2018

Choose a reason for hiding this comment

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

I’m going to see if I can get automatic CSP support in Nexjs today including nonces for generates styles.

<Head nonce='test-nonce'>
{csp ? <meta httpEquiv='Content-Security-Policy' content={csp} /> : null}
<Head>
{this.props.withCSP ? <meta httpEquiv='Content-Security-Policy' content="default-src 'self'; style-src 'self' 'unsafe-inline';" /> : null}
Copy link
Contributor

@dbo dbo Aug 28, 2018

Choose a reason for hiding this comment

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

This is actually weakening the current CSP of the test. Why are you suggesting this? It does not make sense to me, users should rather define a hash in their CSP for the exceptional inline scripts they need, or nonces (if they run a server to reliably generate fresh nones, of course).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not true at all. Look closely at your CSP policy when you were using hashes: https://github.com/zeit/next.js/blob/canary/test/integration/app-document/pages/_document.js#L38

Inline styles is a different issue and a much more minor issue as it’s only a tiny visual security hole (instead of something being executed).

Copy link
Contributor

Choose a reason for hiding this comment

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

You're right, I misread that rule.

@@ -0,0 +1,17 @@
window.module = {}
window.__NEXT_DATA__ = JSON.parse(
Copy link
Member

Choose a reason for hiding this comment

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

This can be done in client/index.js right 🤔

Copy link
Contributor Author

@dav-is dav-is Aug 28, 2018

Choose a reason for hiding this comment

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

No. There’s a function appended to the beginning of each page.js and they load async so they could be called at any moment. This means that these functions need to be defined before any other scripts are loaded, which is only possible by loading it first as the client/index.js could be loaded third and two pages have loaded first and there’s no function to register them.

Also, module={} has to be defined before any webpack files load as they begin with module.exports and will say module is undefined. I’m guessing these are all the hoops you had to jump through to get asynchronous loading working.

Copy link
Member

Choose a reason for hiding this comment

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

window.__NEXT_DATA__ = JSON.parse(

I meant reading the __NEXT_DATA__, the rest still has to be defined beforehand like you said.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@@ -0,0 +1,17 @@
window.module = {}
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if this is still needed with the latest webpack, it might not be needed anymore

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removing it breaks everything.

Copy link
Member

Choose a reason for hiding this comment

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

Fixed here: #5093

{page !== '/_error' && <link rel='preload' href={`${assetPrefix}/_next/static/${buildId}/pages${pagePathname}`} as='script' nonce={this.props.nonce} />}
<link rel='preload' href={`${assetPrefix}/_next/static/${buildId}/pages/_app.js`} as='script' nonce={this.props.nonce} />
<link rel='preload' href={`${assetPrefix}/_next/static/${buildId}/pages/_error.js`} as='script' nonce={this.props.nonce} />
<link rel='preload' href={`${assetPrefix}/_next/static/runtime/bootstrap.js`} as='script' />
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there an agreement about a further script request, or can the bootstrap code be integrated into a bundle?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's no guarantee about execution order of scripts, so it would be hard to ensure that the functions in bootstrap.js are run first. I looked into adding it into the webpack runtime, but it seemed more complicated than it was worth.

render () {
const { staticMarkup, assetPrefix, devFiles, __NEXT_DATA__ } = this.context._documentProps
const { page, pathname, buildId } = __NEXT_DATA__
const pagePathname = getPagePathname(pathname)

__NEXT_DATA__.cleanPathname = htmlescape(__NEXT_DATA__.pathname);
Copy link
Member

Choose a reason for hiding this comment

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

What is the possible attack vector here, assuming that's the reason you're escaping the pathname 🤔 pathname is already part of __NEXT_DATA__, and __NEXT_DATA__ gets htmlescaped too

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 didn’t want to have to bundle htmlescape into the the bootstrap file. I just escape the value early: https://github.com/zeit/next.js/blob/canary/server/document.js#L192

@dav-is dav-is changed the title Move Inline Script to Bootstraping File to Support Strong CSP WIP: Move Inline Script to Bootstraping File to Support Strong CSP Aug 28, 2018
@dav-is
Copy link
Contributor Author

dav-is commented Dec 11, 2018

Turns out that CSP doesn't like

<iframe>
  <script>alert('test')</script>
</iframe>

so react-error-overlay doesn't work without having script-src 'unsafe-inline'. We would need to create a PR that uses <iframe src="/error_overlay"></iframe> which I'm not sure how feasible it would be... maybe it's time to implement a custom Next.js error overlay? 😞

Also, we still have a few tests and error pages that use inline styles.

@timneutkens
Copy link
Member

cc @Timer @iansu

@Timer
Copy link
Member

Timer commented Dec 11, 2018

We're more than happy to implement any changes needed to make the overlay CSP compatible.

I'm not a big fan of facebook/create-react-app#5943 because it looks like the user mounting the overlay will have to be aware of what styles to apply.

How do libraries like styled-components do this in a client-side friendly way (or is this behavior special to iframe)? Can we just make the default behavior CSP friendly for everyone?

We've been slow to adopt CSP because the overlay is mainly a development tool, not a production addon. I'm open to any proposals.


Can we still do things like this?
https://github.com/facebook/create-react-app/blob/8dc1902f2f655e26fe86647ea2cb444db69dd694/packages/react-error-overlay/src/utils/dom/absolutifyCaret.js

@dav-is
Copy link
Contributor Author

dav-is commented Dec 11, 2018

@Timer The root iframe has styles applied to it inline. The PR allows us to move the styles into <style nonce=""> react-error-overlay isnt scoped to do something like this and the styles are just setting top: 0 and basic things like that that don't change.

The problem with using an iframe is that you can't write everything inline. You'd need to create a new page to view in an iframe and use some kind of API to communicate between the iframe and the runtime error. Google does something kind of similar with Auth although I don't know how they do it...

@Timer
Copy link
Member

Timer commented Dec 11, 2018

Are pages allowed to inject their own new style tags? Can we create a new style tag in the current page and then assign it to the iframe using loadingIframe.className = "";?

@dav-is
Copy link
Contributor Author

dav-is commented Dec 11, 2018

In order to have safe CSP styles all styles need to either be in a stylesheet externally (which could be done inside of react-error-overlay) or internally with a nonce (<style nonce="secretThatAppHandles">). So an improvement of my PR for react error overlay could serve a stylesheet just for the root iframe element. That would solve the unsafe-inline of styles.

The unsafe-inline of scripts is a bit more serious and difficult to solve. I'm diving deeper into how iframes work now

@dav-is
Copy link
Contributor Author

dav-is commented Dec 11, 2018

@Timer I wrote a small html page to demonstrate what I mean. It's sort of a complicated setup division that makes me question if we should even be using an iframe for this...

Code: https://github.com/dav-is/csp-iframe-example
Live Code: https://dav-is.github.io/csp-iframe-example/

Also, you meantioned if you can still do this and you can by using c.className = c.className + ' absolute'; and defining that change in your CSS file.

The benefit of having a strict CSP during development is so you can catch issues before testing the production code. You could build up a concept that doesn't work with your policy (that could be determined and enforced as a team) and you realize it doesn't work with the policy after you finished and ran tests. Or maybe someone doesn't have reliable production build tests and a CSP bug slips through because it worked in development mode.

@timneutkens timneutkens changed the title Strong CSP Support [WIP] Strong CSP Support Dec 20, 2018
@Jero786
Copy link
Contributor

Jero786 commented Jan 11, 2019

There is some update from this PR? there would be awesome to be in the next release version.

@timneutkens
Copy link
Member

This is not going to land before v8. Will be worked on at a later time.

@baldurh
Copy link

baldurh commented Feb 19, 2019

I’m happy to see there’s progress on this issue 🙂 Before I dive in and try it myself, is there a way for me to manually add CSP to my application or is there still some issues in v8 that prevent that? In the v8 release blog post they mention that before v8 it was only possible using script-src 'unsafe-inline' but now you should be able to use script-src 'self'. Is there anything else I can expect to stop me from manually adding the headers to my responses?

@dav-is
Copy link
Contributor Author

dav-is commented Feb 19, 2019

@baldurh CSP works in production mode, but you'll run into some errors in development, so you'll need to test your CSP on your production build for now. There's also support for style-src in this PR

@timneutkens
Copy link
Member

Going to close this PR as it's outdated. We'll tackle it later on 👌 @dav-is has since joined the Next.js team 👍

@monokrome
Copy link

@timneutkens Hi! Can you link the follow-up PR for those of us who are following along from past issues here? I'm wondering what the status of CSP in Next is, but this seems like the end of the trail for people who came from the previous issue right now 🙀

@ghost
Copy link

ghost commented Jul 6, 2020

any new? documentation?

@timneutkens
Copy link
Member

Here's the example: https://github.com/vercel/next.js/tree/canary/examples/with-strict-csp. You can search the releases for the related PRs that removed inline JS.

@javidjamae
Copy link

@conioX Looks like you can include Content Security Policy headers in NextJS 9.5

https://nextjs.org/blog/next-9-5#headers
https://nextjs.org/docs/api-reference/next.config.js/headers

@rogerweb
Copy link

The last item in the above TODO list reads

"Add nonce support to other CSS-in-JS libraries with updated examples"

and the provided example only mentions

Note: There are still valid cases for using a nonce in case you need to inline scripts or styles for which calculating a hash is not feasible.

Is this something that we can expect in the near future? I would appreciate it very much as I'm struggling to make CSP to work with Material-UI.

@vercel vercel locked as resolved and limited conversation to collaborators Jan 30, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.