jagaapple / next-secure-headers

Sets secure response headers for Next.js.
MIT License
310 stars 13 forks source link

Nonce & Hash support for CSP Level 2 #50

Open moshie opened 3 years ago

moshie commented 3 years ago

🌱 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.


jagaapple commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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.