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 { gsspWithNonceAppliedToCsp } from "@next-safe/middleware/dist/document";
// wrap data fetching with gsspWithNonceAppliedToCsp
export const getServerSideProps = gsspWithNonceAppliedToCsp(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;
⚠️

gsspWithNonceAppliedToCsp 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,
};

Hash-based Strict CSP and Incremental Static Regeneration (ISR)

Add the following code to the top of every route with getStaticProps that uses revalidate (including res.revalidate or res.unstable_revalidate for On-demand ISR):

export const config = {
unstable_includeFiles: [".next/static/chunks/**/*.js"],
};
💡

If you like to know in detail what Incremental Static Regeneration (ISR) is and how it works, read the docs from Vercel

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.

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

⚠️

Hash-based Strict CSP doesn't really work for Firefox and Safari, as they seem to be not fully CSP-3 compliant yet. Using strict-dynamic messes up SRI validation for them, such that it doesn't work with scripts that have both a src and integrity attribute. The strictDynamic middleware handles this by opting out Firefox and Safari from Strict CSP for static routes (Hash-based) and uses the fallbackSrc instead, so visitors that use Safari or Firefox won't have Strict CSP protection when they enter your site/app via a static route. When they enter your site/app via a dynamic route, they have Strict CSP protection with Nonce-based Strict CSP. More details about that problem can be found here and here.

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