Strict CSP configuration

Base setup

create a file middleware.js in your Next.js project folder:

// middleware.js
import {
chainMatch,
isPageRequest,
csp,
strictDynamic,
} from "@next-safe/middleware";
const securityMiddleware = [
csp({
// your CSP base configuration with IntelliSense
// single quotes for values like 'self' are automatic
directives: {
"img-src": ["self", "data:", "https://images.unsplash.com"],
"font-src": ["self", "https://fonts.gstatic.com"],
},
}),
strictDynamic(),
];
export default chainMatch(isPageRequest)(...securityMiddleware);

create a file pages/_document.js in your Next.js project folder:

// pages/_document.js
import {
getCspInitialProps,
provideComponents,
} from "@next-safe/middleware/dist/document";
import Document, { Html, Main } from "next/document";
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await getCspInitialProps({ ctx });
return initialProps;
}
render() {
const { Head, NextScript } = provideComponents(this.props);
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}

For every page under pages that uses getServerSideProps for data fetching:

import { gsspWithNonce } from "@next-safe/middleware/dist/document";
// wrap data fetching with gsspWithNonce
// to generate a nonce for CSP
export const getServerSideProps = gsspWithNonce(async (ctx) => {
return { props: { message: "Hi, from getServerSideProps" } };
});
// the generated nonce also gets injected into page props
const Page = ({ message, nonce }) => <h1>{`${message}. Nonce ${nonce}`}</h1>;
export default Page;
⚠️

gsspWithNonce is necessary with Next.js 12.2 as there seems no longer a way to automatically tell apart routes with getStaticProps + revalidate (ISR) from routes with getServerSideProps. But that is needed to make the right decision between Hash-based or Nonce-based Strict CSP.

Default CSP directives

Those are the minimal and sensible defaults this package provides as the common base for Strict CSPs:

const defaults = {
directives: {
"default-src": ["self"],
"object-src": ["none"],
"base-uri": ["none"],
},
isDev: process.env.NODE_ENV === "development",
reportOnly: !!process.env.CSP_REPORT_ONLY,
};

CSP Converter

This is a litte tool to convert CSP header values to the object format expected by this lib. This can be used when working with tools that can record CSP header values from browsing your site.

💡

One such tool is the Laboratory Extension for Firefox: https://addons.mozilla.org/en-US/firefox/addon/laboratory-by-mozilla/. It allows you to detect and record sources from browsing your site.

{
  "directives": {
    "default-src": [
      "self"
    ],
    "object-src": [
      "none"
    ],
    "base-uri": [
      "none"
    ]
  }
}

Add custom scripts

Just add them with next/script and strategies afterInteractive or lazyOnLoad on the pages where you need them. If you want to include a script in all pages, add it to your pages/app.js.

Custom scripts that must run before the page is interactive, have to be added to pages/_document.js, with <Script strategy="beforeInteractive /> or with a regular <script>{inlineCodeString}</script> tag as child of <Head>.

If you stick to those recommendations, all your script usage will work automatically with the hybrid Strict CSP capabilites provided by this package.

The following files serve as examples for script usage:

🚫

NEVER add unsafe (inline) script code from dynamic data anywhere within <Head> of pages/_document.js / next/head or <Script> of next/script. Scripts in those places will be trustified for Strict CSP by this package during SSR.

How this behaves behind the scenes

<Script>'s with strategies afterInteractive and lazyOnLoad will become trusted by transitive trust propagation of strict-dynamic and so will be all scripts that they load dynamically, etc. That should cover the majority of use cases.

<Script>'s with strategy beforeInteractive you place in _document.jsand inline <script>'s you place as children of <Head> in _document.js are automatically picked up for Strict CSP by this package.

What this package will do with such scripts, depends:

getServerSideProps (Nonce-based)

the script will eventually receive the nonce.

getStaticProps (Hash-based)

  1. The script loads from src and has an integrity attribute: The integrity attribute/hash will be picked up for CSP. Don't forget to set { crossOrigin: "anonymous" } in next.config.js, else the SRI validation will fail.
💡

A nice tool to conveniently get the hash for such scripts: https://www.srihash.org/.

  1. The script loads from src and doesn't have an integrity attribute: The script will be replaced by an inline proxy script that loads the script. The hash of this proxy script will be picked up for CSP. The actual script eventually becomes trusted by transitive trust propagation of strict-dynamic.
💡

The default behavior is to replace all scripts with a src attribute by an inline loading proxy during SSR to support Firefox and Safari (they mess up SRI validation with strict-dynamic, see here and here). To opt-out from this behavior, you can set hashBasedByProxy: false in getCspInitialProps.

  1. The script is an inline script: The inline code of the script will be hashed, the hash will be set as integrity attribute of the inline script and the hash will be picked up by CSP.

Avoid unsafe inline styles (CSS-in-JS)

This package tries to provide a best effort solution to this, with a strictInlineStyles middleware. The e2e test app of this package comes with a setup that uses both twin.macro + Stitches and Mantine without unsafe-inline in style-src.

💡

For more information, visit a discussion on GitHub about problems and their solution for a setup with Mantine, a React component library that uses emotion CSS-in-JS under the hood.

The following files serve as the references for such setups:

⚠️

This package might not always be able to solve this issue, as this is highly dependent on the actual CSS-in-JS framework and 3rd party libs (dynamically inject inline styles?) you use.

Set additional security headers

💡

A good listing with explanations can be found in the Next.js docs

There are more security headers in addition to CSP. To set them conveniently, you can use the nextSafe middleware that wraps the next-safe package. Use it with CSP disabled and use the csp middleware for your CSP configuration instead:

// middleware.js
import {
chainMatch,
isPageRequest,
csp,
nextSafe,
strictDynamic,
} from "@next-safe/middleware";
const securityMiddleware = [
nextSafe({ disableCsp: true }),
csp(),
strictDynamic(),
];
export default chainMatch(isPageRequest)(...securityMiddleware);
💡

The configuration options of the nextSafe middleware are the same as documented at https://trezy.gitbook.io/next-safe/usage/configuration