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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nonce & Hash support for CSP Level 2 #50

Open
1 of 2 tasks
moshie opened this issue Mar 26, 2021 · 3 comments
Open
1 of 2 tasks

Nonce & Hash support for CSP Level 2 #50

moshie opened this issue Mar 26, 2021 · 3 comments

Comments

@moshie
Copy link

moshie commented Mar 26, 2021

馃尡 Feature Request

Is your feature request related to a problem? Please describe.

I am attempting to implement a CSP into my app and not use unsafe-inline.

The way I understand CSP is that for every HTTP request I need to generate a base64 nonce / hash which gets put on the script tag and in the CSP header prefixed with nonce-.

Describe the solution you'd like

I am not entirely sure on the solution I need here.
In my opinion it would be nice to have a solution which allows me to access a generated base64 string and pass it into both the headers and the script tags

Describe alternatives you've considered

I haven't considered any way of approaching this yet but I'll continue to try.


  • I've tried to find similar issues and pull requests
  • I would like to work on this feature 馃挭馃徎
@jagaapple
Copy link
Owner

Next.js components ( NextScript and Head in _document.jsx ) support to accept nonce Prop in order to implement them like the following.

// _document.jsx
import { randomBytes } from "crypto";
import Document, { Html, Head, Main, NextScript } from "next/document";

export default class extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    const nonce = randomBytes(128).toString("base64");

    return { ...initialProps, nonce };
  }

  render() {
    const { nonce } = this.props;
    const csp = `script-src 'self' 'unsafe-inline' 'unsafe-eval' https: http: 'nonce-${nonce}' 'strict-dynamic'`;

    return (
      <Html>
        <Head nonce={nonce}>
          <meta httpEquiv="Content-Security-Policy" content={csp} />
        </Head>
        <body>
          <Main />
          <NextScript nonce={nonce} />
        </body>
      </Html>
    );
  }
}

But to generate a hash for every request, Server-Side Rendering is required. In Static Site Generation, a hash is generated on build time, and it doesn't have sufficient effect for prevention. Also, in Incremental Static Regeneration, a hash will be generated on rebuild time, and it doesn't as well.

I've been considering to support nonce from the beginning, but it's currently on hold because I don't want to implement half-baked security features in next-secure-headers. If there is more need, I'd like to implement it. 馃檪

@dimisus
Copy link

dimisus commented Apr 18, 2021

EDIT: my example below doesn't seem to work because you cannot dynamically set nonce in headers in next.config :(

Until there is an gSSP in _app.js I will go with this CSP rule per page basis.

Nonce is then accessible in _document.js via res.locals.nonce (careful, it will not be present in pages that are not wrapped or have getStaticProps/static pages)

import { createContentSecurityPolicyHeader } from 'next-secure-headers/lib/rules/content-security-policy';

const withCSPNonce = curry((gSSP, context) => {
  if (gSSP && context) {
    return Promise.resolve(gSSP(context)).then((pipedProps) => {
      if (pipedProps.props) {
        const nonce = v4();

        const CSP = createContentSecurityPolicyHeader({
          directives: {
            baseUri: ["'self'"],
            defaultSrc: ["'self'", ...srcWhitelist],
            styleSrc: ["'self'", "'unsafe-inline'", ...srcWhitelist],
            imgSrc: ["'self'", 'data:', 'blob:', 'https:', ...srcWhitelist],
            objectSrc: ["'none'"],
            scriptSrc: [
              `'nonce-${nonce}'`,
              "'strict-dynamic'",
              "'unsafe-inline'",
              `${(isDev && "'unsafe-eval'") || 'https:'}`,
              ...srcWhitelist,
            ],
          },
        });

        context.res.setHeader('content-security-policy', CSP.value);
        context.res.locals = { nonce };

        return {
          props: {
            ...pipedProps.props,
            nonce,
          },
        };
      }

      return pipedProps;
    });
  }

  throw Error('Either context or gSSP is not provided');
});

Thanks for the package. I was breaking my head around CSP in Next.js. I wanted to get rid of my custom express server where I used Helmet.

I have slightly extended your example since I also need the nonce via script.setAttributte('nonce', nonce) for inline scripts otherwise safari throws an issue and so on.

I also like to apply the header to the response and not the meta tag:

next.config

const { v4 } = require('uuid')

//.... next.config options
  async headers() {
    return [
      {
        source: '/:path*', // attention here, the docs are incorrect (.*) <- not possible
        headers: createSecureHeaders({
          //...options
          contentSecurityPolicy: {
            directives: {
              //...other rules
              scriptSrc: [
                `'nonce-${v4()}'`, // set nonce with the CSP headers
                ...

_document.js

export default class extends Document {
  static async getInitialProps(ctx) {
    
    // get the nonce with a regex from headers in NextJs after applying it in next.config
    const [, nonce = ''] = /(?:nonce-)([a-z0-9-]+)/gi.exec(
      ctx?.res?.getHeader('content-security-policy')
    );
    
     return {
      ...,
      nonce
     }
    ... // apply it in <Head nonce={nonce}> etc from  const { nonce } = this.props;

Per page if needed

const withCSPNonce = gSSP => context => {
  if (gSSP && context) {
    return Promise.resolve(gSSP(context)).then((pipedProps) => {
      if (pipedProps.props) {

        const [, nonce = ''] = /(?:nonce-)([a-z0-9-]+)/gi.exec(
          context?.res?.getHeader('content-security-policy')
        ) ||[]; // same way to get the nonce as in _document.js

        return {
          props: {
            ...pipedProps.props,
            nonce, // apply it to pipedProps from getServerSideProps
          },
        };
      }

      return pipedProps;
    });
  }

  throw Error('Either context or gSSP is not provided');
});

export default withCSPNonce;

In a page you can wrap with the higher order function
Your pageProps in _app.js or your page will have pageProps.nonce from the response headers

export const getServerSideProps = withCSPNonce(async(context) => {
  //... logic
  return {
     props: {
          .....
     }
  }
})

@quantizor
Copy link

Next.js components ( NextScript and Head in _document.jsx ) support to accept nonce Prop in order to implement them like the following.

// _document.jsx
import { randomBytes } from "crypto";
import Document, { Html, Head, Main, NextScript } from "next/document";

export default class extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    const nonce = randomBytes(128).toString("base64");

    return { ...initialProps, nonce };
  }

  render() {
    const { nonce } = this.props;
    const csp = `script-src 'self' 'unsafe-inline' 'unsafe-eval' https: http: 'nonce-${nonce}' 'strict-dynamic'`;

    return (
      <Html>
        <Head nonce={nonce}>
          <meta httpEquiv="Content-Security-Policy" content={csp} />
        </Head>
        <body>
          <Main />
          <NextScript nonce={nonce} />
        </body>
      </Html>
    );
  }
}

But to generate a hash for every request, Server-Side Rendering is required. In Static Site Generation, a hash is generated on build time, and it doesn't have sufficient effect for prevention. Also, in Incremental Static Regeneration, a hash will be generated on rebuild time, and it doesn't as well.

I've been considering to support nonce from the beginning, but it's currently on hold because I don't want to implement half-baked security features in next-secure-headers. If there is more need, I'd like to implement it. 馃檪

This sounds like a good candidate for generation by something like Netlify edge lambdas. Essentially create a known placeholder string in your scripts and replace it dynamically with a generated nonce.

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

No branches or pull requests

4 participants