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

Inline styles in Next 10 don't work with strict Content-Security-Policy #18557

Closed
sophiekoonin opened this issue Oct 30, 2020 · 18 comments · Fixed by #19150
Closed

Inline styles in Next 10 don't work with strict Content-Security-Policy #18557

sophiekoonin opened this issue Oct 30, 2020 · 18 comments · Fixed by #19150
Assignees
Milestone

Comments

@sophiekoonin
Copy link

Bug report

Describe the bug

Recent versions of Next are using inline styles, which break our apps because we block style-src: unsafe-inline in our Content-Security-Policy header. In fact, the default behaviour of any CSP is to block unsafe inline styles. While the risk may be less than with unsafe inline scripts, there is still a risk: see this StackOverflow answer.

We have extremely high security requirements at my company so it's not really a matter of easily being able to disable unsafe-inline, and I'm aware that the future intention is to move more of the styling inline. If we can't upgrade Next because of this, we'll either be stuck on an old version and potentially vulnerable that way, or we'll have to consider alternatives (which I really don't want to do!).

To Reproduce

Steps to reproduce the behavior, please provide code snippets or a repository:

  1. Create a Content-Security-Policy header in next.config.js with style-src: 'self'; (i.e. without unsafe-inline explicitly enabled)
  2. Run the app in production mode
  3. See error in console:
Refused to apply inline style because it violates the following Content Security Policy directive: "style-src 'self'". Either the 'unsafe-inline' keyword, a hash, or a nonce ('nonce-...') is required to enable inline execution.

Expected behavior

If inline styles can't be disabled, there should be the ability to add a nonce. This nonce will need to be regenerated on every request, and be able to be injected into the content-security-policy header.

I can see that the Head component in next/document accepts a nonce prop but this doesn't appear to apply to Next-generated inline styles. Additionally, when trying to set the nonce prop I found it was being set on the link elements as a blank attribute - the actual nonce value was not being passed in, despite it being present in this.props.nonce (I tested with some console.logging in the compiled Next code).
image

System information

  • Version of Next.js: [e.g. 6.0.2] 10.0.0
@Timer
Copy link
Member

Timer commented Nov 13, 2020

@sophiekoonin While we're working on official (fully managed) CSP control within Next.js itself, we fixed the ability for you to pass nonce to <Head> in _document and have Next.js use it!

This should allow you to generate a valid CSP. You probably need to move from a headers approach to a <meta /> tag though if possible (so you can randomize it each request).

@paambaati
Copy link
Contributor

we're working on official (fully managed) CSP control within Next.js itself

@Timer This is great news! Is there an RFC or an issue I could subscribe to for updates?

@ivan-kleshnin
Copy link
Contributor

ivan-kleshnin commented Nov 14, 2020

@paambaati you can use the following approach right now:

import NextDocument, {Html, Head, Main, NextScript} from "next/document"

let prod = process.env.NODE_ENV == "production"

let csp = ``
csp += `base-uri 'self';`
csp += `form-action 'self';`
csp += `default-src 'self';`
csp += `script-src 'self' ${prod ? "" : "'unsafe-eval'"};`                    // NextJS requires 'unsafe-eval' in dev (faster source maps)
csp += `style-src 'self' https://fonts.googleapis.com 'unsafe-inline' data:;` // NextJS requires 'unsafe-inline'
csp += `img-src 'self' https://*.githubusercontent.com https://paqmind.imfast.io data: blob:;`
csp += `font-src 'self' https://fonts.gstatic.com;`  // TODO
csp += `frame-src *;` // TODO
csp += `media-src *;` // TODO

// require-trusted-types-for 'script';" TODO

let referrer = "strict-origin"

export default class Document extends NextDocument {
  render () {
    return <Html prefix="og: http://ogp.me/ns#" lang="ru">
      <Head>
        <meta httpEquiv="Content-Security-Policy" content={csp}/>
        <meta name="referrer" content={referrer}/>
      </Head>
      <body>
        <Main/>
        <div id="modal"/>
        <NextScript/>
      </body>
    </Html>
  }
}

You'll need to replace my static domains, etc. with yours obviously. I kept mine for extra context.
Those are not the strictest rules possible but we score A+ in Mozilla.observatory with them.

As @Timer mentioned, one the latest NextJS releases also removes the need for unsafe-inline but I personally haven't tried it yet.

@Timer
Copy link
Member

Timer commented Nov 14, 2020

Something along these lines, copying the above code:

import NextDocument, { Html, Head, Main, NextScript } from "next/document";
import { randomBytes } from "crypto";

let prod = process.env.NODE_ENV == "production";

function getCsp(nonce) {
  let csp = ``;
  csp += `base-uri 'self';`;
  csp += `form-action 'self';`;
  csp += `default-src 'self';`;
  csp += `script-src 'self' ${prod ? "" : "'unsafe-eval'"};`; // NextJS requires 'unsafe-eval' in dev (faster source maps)
  csp += `style-src 'self' https://fonts.googleapis.com 'nonce-${nonce}' data:;`; // NextJS requires 'unsafe-inline'
  csp += `img-src 'self' https://*.githubusercontent.com https://paqmind.imfast.io data: blob:;`;
  csp += `font-src 'self' https://fonts.gstatic.com;`; // TODO
  csp += `frame-src *;`; // TODO
  csp += `media-src *;`; // TODO
  return csp;
}

// require-trusted-types-for 'script';" TODO

let referrer = "strict-origin";

export default class Document extends NextDocument {
  render() {
    // Regenerated every render:
    const nonce = crypto.randomBytes(8).toString("base64");
    return (
      <Html prefix="og: http://ogp.me/ns#" lang="ru">
        <Head nonce={nonce}>
          <meta httpEquiv="Content-Security-Policy" content={getCsp(nonce)} />
          <meta name="referrer" content={referrer} />
        </Head>
        <body>
          <Main />
          <div id="modal" />
          <NextScript />
        </body>
      </Html>
    );
  }
}

@ivan-kleshnin
Copy link
Contributor

ivan-kleshnin commented Nov 14, 2020

@Timer does Styled-JSX support nonce- stuff? I still get warnings with this apporach (without unsafe-inline), most probably caused by Styled-JSX.

@hades200082
Copy link

I don't think the OP's issue has been fixed here... next/image still outputs inline styles which means that you still have to use unsafe-inline for style-src

@ranyehushua
Copy link

I don't think the OP's issue has been fixed here... next/image still outputs inline styles which means that you still have to use unsafe-inline for style-src

Looks like the issue was solved and merged a couple weeks before this comment: #19150

nonce attribute is pulled from the head tag and applied to style tags that are later generated and inserted.

@Manc
Copy link
Contributor

Manc commented Jan 27, 2021

I'm looking into this problem at the moment and I get the feeling that the solution is not appropriate. I don't consider myself an expert on this, so please correct me if I'm wrong. So here's the problem:

For a solution based on nonce it is essential that the value is randomly generated for every single request.

This means:

  1. The solution proposed above, generating the nonce value in the render() method of _document, does not work for statically rendered HTML pages as the nonce value will be generated once at build time and then remains the same until it is built again.
  2. Even if one were to disable static rendering in Next.js completely (which would be pretty sad), server-side caching would also have to be disabled, i.e. every page really needs to be rendered for every single request.

If I'm not missing anything here, this pretty much defeats one of the main objectives of Next.js, which is to make page loads as fast as possible.

As I said earlier today here, I don't know the background of why the way CSS is now being loaded/pre-loaded has been changed recently. From what I can tell, everything worked fine until at least version 9.4, without the need for fetch-ing CSS and injecting styles.

Tiny speed improvements should not stand above best-practice security measures. Like I said, I don't know the background is, but I think more needs to be done here. At the moment, if I want to stay on version 10, I have no choice but to use a style-src: 'unsafe-inline' CSP.

@moises-marquez
Copy link

This doesn't work with material-ui because the style tag doesn't have the nonce. Any idea on how to add it?

The Next.js example with material-ui injects the styles in _document.js like follows:

MyDocument.getInitialProps = async (ctx) => {
  const sheets = new ServerStyleSheets();
  const originalRenderPage = ctx.renderPage;

  ctx.renderPage = () =>
    originalRenderPage({
      enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
    });

  const initialProps = await Document.getInitialProps(ctx);
  return {
    ...initialProps,
    // Styles fragment is rendered after the app and page rendering finish.
    styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()],
  };
};

@lvnam96
Copy link

lvnam96 commented Mar 22, 2021

Additionally, when trying to set the nonce prop I found it was being set on the link elements as a blank attribute - the actual nonce value was not being passed in, despite it being present in this.props.nonce

@sophiekoonin it's not blank, just not visible to you via dev tools (see this answer on SO: https://stackoverflow.com/a/55673767/5805244).

@BleddP
Copy link

BleddP commented Apr 9, 2021

Something along these lines, copying the above code:

import NextDocument, { Html, Head, Main, NextScript } from "next/document";
import { randomBytes } from "crypto";

let prod = process.env.NODE_ENV == "production";

function getCsp(nonce) {
  let csp = ``;
  csp += `base-uri 'self';`;
  csp += `form-action 'self';`;
  csp += `default-src 'self';`;
  csp += `script-src 'self' ${prod ? "" : "'unsafe-eval'"};`; // NextJS requires 'unsafe-eval' in dev (faster source maps)
  csp += `style-src 'self' https://fonts.googleapis.com 'nonce-${nonce}' data:;`; // NextJS requires 'unsafe-inline'
  csp += `img-src 'self' https://*.githubusercontent.com https://paqmind.imfast.io data: blob:;`;
  csp += `font-src 'self' https://fonts.gstatic.com;`; // TODO
  csp += `frame-src *;`; // TODO
  csp += `media-src *;`; // TODO
  return csp;
}

// require-trusted-types-for 'script';" TODO

let referrer = "strict-origin";

export default class Document extends NextDocument {
  render() {
    // Regenerated every render:
    const nonce = crypto.randomBytes(8).toString("base64");
    return (
      <Html prefix="og: http://ogp.me/ns#" lang="ru">
        <Head nonce={nonce}>
          <meta httpEquiv="Content-Security-Policy" content={getCsp(nonce)} />
          <meta name="referrer" content={referrer} />
        </Head>
        <body>
          <Main />
          <div id="modal" />
          <NextScript />
        </body>
      </Html>
    );
  }
}

I tried the above solution and it works great, as it blocks the scripts which are not whitelisted by a nonce.

However for some reason the Content Security Policy header does not show up on the response headers during HTTP request. Is that normal behaviour or did I miss something?

PS I am using a custom Express server, but was unable to get the CSP working with Helmet as I was unable to get req.locales (req returns undefined in _document.js). Hence I think the above solution should work for now?

@mwmcode
Copy link
Contributor

mwmcode commented Apr 9, 2021

@BleddP I think they don't show in the http header because they're set in <meta> tags

CTRL/CMD + SHIFT + U to view document source code, you should be able to see the policies there

@ivan-kleshnin
Copy link
Contributor

ivan-kleshnin commented Apr 10, 2021

Yeah, as @mcha-dev said. Some security analizers don't check for meta tags but that's an issue with those services. I even personally asked 1 or 2 of them to fix their algorithms and it was done. According to standards, meta and headers are interchangeable for this particular piece of data.

@jota12x
Copy link

jota12x commented Apr 21, 2021

However for some reason the Content Security Policy header does not show up on the response headers during HTTP request. Is that normal behaviour or did I miss something?

This is because the meta tag and HTTP headers are two different approaches for the same result. Nevertheless, it is still recommended to go for the headers one as they provide more features.

Reference: https://content-security-policy.com/examples/meta/

@Choc13
Copy link

Choc13 commented Jun 30, 2021

This is mostly working for me, in that I can see that nonces are being added to every script and inline style that is being injected by Next which is great. However, I've found that a few Next components add style attributes. In particular the Next/Image component as can be seen here in the source. This then causes a problem for a nonce based CSP.

The best solution I've found is to write a csp like this

style-src 'self' 'unsafe-inline';
style-src-attr 'unsafe-inline';
style-src-elem 'self' 'nonce-somevalue' 'unsafe-inline';

So that in modern browsers that support style-src-attr and style-src-elem I only have to weaken thestyle-src-attr directive. Then I just supply the style-src directive as a fallback for older browsers.

Does anyone else have a better workaround for this? Or would it be possible for Next to stop using style attributes?

miya-start added a commit to miya-start/next-chat that referenced this issue Aug 28, 2021
@guydumais
Copy link

You're right @Manc, as Lukas Weichselbaum from web.dev said in a recent post, nonce-based CSP only works if the number is not guessable and newly generated at runtime for every response.

That's why I've come to build the next-strict-csp package on NPM to implement a hash-based CSP with Next.js the right way.

Enjoy!

@UnderTheMoonspell
Copy link

UnderTheMoonspell commented Oct 13, 2021

The solutions proposed seem to work with the scripts injected by next, also with Material-UI, but I'm using CSS Modules, and none of my style tags have the nonce (next v11):

image

abhinavkgrd added a commit to ente-io/photos-web that referenced this issue Dec 2, 2021
@balazsorban44
Copy link
Member

This issue has been automatically locked due to no recent activity. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@vercel vercel locked as resolved and limited conversation to collaborators Jan 27, 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 a pull request may close this issue.