Implementation av CSP med kryptografisk nonce i Next.js

Som många andra har jag fastnat rejält för Next.js. Jag uppskattar och diggar verkligen allt som Vercel gör och använder själv flera av deras ramverk och verktyg.

Idag är v13.1.6 den senaste versionen av Next.js och även om det finns utförlig dokumentation för ramverket så saknade jag information om hur man kan implementera en nonce-baserad Content Security Policy.

Varför använda en nonce-baserad lösning?

Fördelen med en nonce-baserad lösning är att varje skript som används på sidan får en nonce tilldelad. En kryptografiskt genererad sträng som läggs till i webbplatsens Content Security Policy för att enbart tillåta skript som har sådan nonce som tillåts.

Om en angripare exempelvis skulle lyckas köra ett skript med skadlig kod på min webbplats så hade skriptet blockerats, eftersom den saknar den tillåtna noncen som krävs.

Implementation

Manuellt exempel med crypto-biblioteket

Nedan finner ni ett exempel på hur en manuell implementation kan se ut. I koden ser vi att vi använder det inbyggda crypto-biblioteket för att generera en hash, för att därefter skapa en Content Security Policy som skickas med som HTTP-headern när sidan laddas.

// pages/_document.tsx
import Document, {Head, Html, Main, NextScript} from 'next/document'
import crypto from 'crypto';
import {v4} from 'uuid';
import Script from "next/script";

const generateCsp = (): {csp: string, nonce: string} => {
    const hash = crypto.createHash('sha256');
    hash.update(v4());
    const nonce = hash.digest('base64');
    const csp = `
		default-src 'self';
		script-src 'self' 'nonce-${nonce}';
		frame-src 'self';
		base-uri 'self';
		block-all-mixed-content;
		font-src 'self' https: data:;
		img-src * 'self' data: https;
		object-src 'none';
		script-src-attr 'none';
		style-src 'self' https: 'unsafe-inline';
		upgrade-insecure-requests;
		`.replace(/\n/g, " ");

    return {csp, nonce};
};

type MyDocumentProps = {
    nonce: string,
}

export default class MyDocument extends Document<MyDocumentProps> {
    static async getInitialProps(ctx) {
        const initialProps = await Document.getInitialProps(ctx)
        const {csp, nonce} = generateCsp();
        const res = ctx?.res
        if (res != null && !res.headersSent) {
            res.setHeader('Content-Security-Policy', csp)
        }
        return {
            ...initialProps,
            nonce,
        }
    }

    render() {
        const {nonce} = this.props;
        return (
            <Html>
                <Head nonce={nonce}>
					{/*
					<Script nonce={nonce}/>
					Fler externa skript...
					*/}
                </Head>
                <body>
	                <Main/>
	                <NextScript nonce={nonce}/>
                </body>
            </Html>
        )
    }
}

Implementation med hjälp av @next-safe/middleware

Npm-paketet @next-safe/middleware är ett utmärkt val som sköter allt det jobbiga åt det, plus att det är mer optimerat jämfört med exemplet ovan. @next-safe/middleware använder nämligen en kombination av hash/nonce beroende på om man använder getStaticProps eller getServerSideProps . Paketet bygger dessutom på middlewares som Next.js har stable-stöd för fr.o.m. Next.js 12.2.

För att implementera en strikt CSP gör du på följande sätt:

  1. Installera paketet genom att köra npm i @next-safe/middleware i terminalen.
  2. Skapa en fil med namnetmiddleware.jsi projektet src-mapp. Filens innehåll kan se ut ungefär såhär beroende på din CSP.
// src/middleware.js
import {chainMatch, csp, isPageRequest, strictDynamic,} from "@next-safe/middleware";

const securityMiddleware = [
    csp({
      directives: {
        "default-src": ["self"],
        "font-src": ["self", "https://fonts.gstatic.com", "https://fonts.googleapis.com"],
        "frame-src": ["self"],
        "base-uri": ["self"],
        "img-src": ["self", 'data:', 'https:'],
        "script-src-attr": ["none"],
        "object-src": ["none"],
        "style-src": ["self"],
        "frame-ancestors": ["none"],
        "form-action": ["none"]
      },
    }),
    strictDynamic(),
  ]
;

export default chainMatch(isPageRequest)(...securityMiddleware); `
  1. I document.jsx uppdaterar du koden så att din CSP skickas med i varje request.
// src/middleware.js
import Document, {Head, Html, Main, NextScript} from 'next/document'
import {getCspInitialProps, provideComponents} from "@next-safe/middleware/dist/document";


class MyDocument extends Document {
  static async getInitialProps(ctx) {
    return await getCspInitialProps({ctx});
  }

  render() {
    const {Head, NextScript} = provideComponents(this.props);

    return (
      <Html lang="en">
        <Head>
			{/* Externa skript m.m. här...*/}
        </Head>
        <body>
	        <Main/>
	        <NextScript/>
        </body>
      </Html>
    )
  }
}

export default MyDocument
  1. Klart!

Hur du testar din CSP

Du testar din Content Security Policy genom att använda exempelvis Googles CSP Evaluator. Försök alltid att göra din policy så strikt som möjligt. En strikt CSP ska ge följande resultat:

Guider

Säkerhet