Strict CSP configuration
Base setup
create a file middleware.js
in your Next.js project folder:
// middleware.jsimport { 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.jsimport { 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 gsspWithNonceAppliedToCspexport const getServerSideProps = gsspWithNonceAppliedToCsp(async (ctx) => { return { props: { message: "Hi, from getServerSideProps" } };});
// the generated nonce also gets injected into page propsconst 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.js
and 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)
- 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" }
innext.config.js
, else the SRI validation will fail.
A nice tool to conveniently get the hash for such scripts: https://www.srihash.org/.
-
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 ofstrict-dynamic
. -
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.jsimport { 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