Skip to content

[nextjs] Fix nonce issues #45794

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 3, 2025
Merged

[nextjs] Fix nonce issues #45794

merged 8 commits into from
Apr 3, 2025

Conversation

Janpot
Copy link
Member

@Janpot Janpot commented Apr 2, 2025

Fixes #45774

  • Add nonce to critical css style tags.
  • Make createEmotionCache available to users and allow passing options.

This makes the pages router example work with strict CSP when making the following alterations:

// ./middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { generateNonce } from "./src/nonce";

export function middleware(request: NextRequest) {
  // Generate unique nonce for each request
  const nonce = generateNonce();

  // Important: Store nonce in headers to access it throughout the request lifecycle
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("x-nonce", nonce);

  // Important: Define CSP directives with nonce
  // Each directive controls different resource types
  const cspHeader = `
  default-src 'self';
  script-src 'self' 'nonce-${nonce}';
  style-src 'self' 'nonce-${nonce}';
  img-src 'self';
  font-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  upgrade-insecure-requests;
`
    .replace(/\s{2,}/g, " ")
    .trim();

  const response = NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });

  if (process.env.NODE_ENV === "production") {
    // Set the CSP header in the response
    response.headers.set("Content-Security-Policy", cspHeader);
  }

  return response;
}
// ./pages/_document.tsx
import * as React from "react";
import {
  Html,
  Head,
  Main,
  NextScript,
  DocumentProps,
  DocumentContext,
} from "next/document";
import {
  DocumentHeadTags,
  DocumentHeadTagsProps,
  documentGetInitialProps,
  createEmotionCache,
} from "@mui/material-nextjs/v15-pagesRouter";
import theme, { roboto } from "../src/theme";

export default function MyDocument(
  props: DocumentProps & DocumentHeadTagsProps & { nonce: string }
) {
  const { nonce } = props;

  return (
    <Html lang="en" className={roboto.className}>
      <Head>
        {/* PWA primary color */}
        <meta name="theme-color" content={theme.palette.primary.main} />
        <link rel="shortcut icon" href="/favicon.ico" />
        <meta name="emotion-insertion-point" content="" />
        <meta name="csp-nonce" content={nonce} />
        <DocumentHeadTags {...props} />
      </Head>
      <body>
        <Main />
        <NextScript nonce={nonce} />
      </body>
    </Html>
  );
}

MyDocument.getInitialProps = async (ctx: DocumentContext) => {
  const { req } = ctx;
  const nonce = req?.headers["x-nonce"];
  if (typeof nonce !== "string") {
    throw new Error('"nonce" header is missing');
  }
  const emotionCache = createEmotionCache({ nonce });
  const finalProps = await documentGetInitialProps(ctx, {
    emotionCache,
  });
  return { ...finalProps, nonce };
};
// ./pages/_app.tsx
import * as React from "react";
import Head from "next/head";
import { AppProps } from "next/app";
import {
  AppCacheProvider,
  createEmotionCache,
} from "@mui/material-nextjs/v15-pagesRouter";
import { ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import theme from "../src/theme";
import { NextPageContext } from "next";
import { IncomingMessage } from "http";

export default function MyApp(props: AppProps & { nonce: string }) {
  const { Component, pageProps, nonce } = props;

  const cache = React.useMemo(() => createEmotionCache({ nonce }), [nonce]);

  return (
    <AppCacheProvider {...props} emotionCache={cache}>
      <Head>
        <meta name="viewport" content="initial-scale=1, width=device-width" />
      </Head>
      <ThemeProvider theme={theme}>
        {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
        <CssBaseline />
        <Component {...pageProps} />
      </ThemeProvider>
    </AppCacheProvider>
  );
}

function getNonce(req?: IncomingMessage) {
  if (req) {
    return req.headers["x-nonce"];
  }
  const nonceMeta = document.querySelector('meta[name="csp-nonce"]');
  if (nonceMeta) {
    return nonceMeta.getAttribute("content");
  }
  return null;
}

MyApp.getInitialProps = async ({ ctx }: { ctx: NextPageContext }) => {
  const { req } = ctx;
  // Note: This only works server-side
  const nonce = getNonce(req);
  if (!nonce) {
    throw new Error("Nonce not found");
  }
  return { nonce };
};

@Janpot Janpot added the nextjs label Apr 2, 2025
@mui-bot
Copy link

mui-bot commented Apr 2, 2025

Netlify deploy preview

https://deploy-preview-45794--material-ui.netlify.app/

Bundle size report

No bundle size changes (Toolpad)
No bundle size changes

Generated by 🚫 dangerJS against ff812d0

@Janpot Janpot marked this pull request as ready for review April 2, 2025 11:22
@Janpot Janpot marked this pull request as draft April 2, 2025 12:41
@Grohden
Copy link
Contributor

Grohden commented Apr 2, 2025

Mayybe its worth a mention in the nextjs (or CSP) docs that you should be doing both things (get nonce at app root & at _document), it shouldn't be hard for people to get a sense that you should be passing the CSP nonce to the emotion cache, but it could be useful to have docs mentioning that so people are less lost when doing it

@Janpot
Copy link
Member Author

Janpot commented Apr 2, 2025

We would accept a PR for that

@Janpot Janpot marked this pull request as ready for review April 2, 2025 13:18
@Janpot Janpot requested a review from a team April 2, 2025 13:18
@Grohden
Copy link
Contributor

Grohden commented Apr 2, 2025

just opened #45798 for the docs

@Janpot Janpot merged commit 8413984 into mui:master Apr 3, 2025
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[CSP] Use the nonce from header manager or receive it as an arg in NextJS?
4 participants