diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index dbfa6ff169f4d..18f7d5c9f028e 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -145,6 +145,7 @@ export type AppRenderContext = AppRenderBaseContext & { flightDataRendererErrorHandler: ErrorHandler serverComponentsErrorHandler: ErrorHandler isNotFoundPath: boolean + nonce: string | undefined res: BaseNextResponse } @@ -362,6 +363,7 @@ async function generateFlight( ctx.clientReferenceManifest.clientModules, { onError: ctx.flightDataRendererErrorHandler, + nonce: ctx.nonce, } ) @@ -841,6 +843,15 @@ async function renderToHTMLOrFlightImpl( parsedFlightRouterState ) + // Get the nonce from the incoming request if it has one. + const csp = + req.headers['content-security-policy'] || + req.headers['content-security-policy-report-only'] + let nonce: string | undefined + if (csp && typeof csp === 'string') { + nonce = getScriptNonceFromHeader(csp) + } + const ctx: AppRenderContext = { ...baseCtx, getDynamicParamFromSegment, @@ -859,6 +870,7 @@ async function renderToHTMLOrFlightImpl( flightDataRendererErrorHandler, serverComponentsErrorHandler, isNotFoundPath, + nonce, res, } @@ -875,15 +887,6 @@ async function renderToHTMLOrFlightImpl( ? createFlightDataResolver(ctx) : null - // Get the nonce from the incoming request if it has one. - const csp = - req.headers['content-security-policy'] || - req.headers['content-security-policy-report-only'] - let nonce: string | undefined - if (csp && typeof csp === 'string') { - nonce = getScriptNonceFromHeader(csp) - } - const validateRootLayout = dev const { HeadManagerContext } = @@ -943,6 +946,7 @@ async function renderToHTMLOrFlightImpl( clientReferenceManifest.clientModules, { onError: serverComponentsErrorHandler, + nonce, } ) @@ -1279,6 +1283,7 @@ async function renderToHTMLOrFlightImpl( clientReferenceManifest.clientModules, { onError: serverComponentsErrorHandler, + nonce, } ) diff --git a/packages/next/src/server/app-render/get-layer-assets.tsx b/packages/next/src/server/app-render/get-layer-assets.tsx index 9c770f1baa4b2..4f580f3c07c08 100644 --- a/packages/next/src/server/app-render/get-layer-assets.tsx +++ b/packages/next/src/server/app-render/get-layer-assets.tsx @@ -43,16 +43,21 @@ export function getLayerAssets({ const ext = /\.(woff|woff2|eot|ttf|otf)$/.exec(fontFilename)![1] const type = `font/${ext}` const href = `${ctx.assetPrefix}/_next/${encodeURIPath(fontFilename)}` - ctx.componentMod.preloadFont(href, type, ctx.renderOpts.crossOrigin) + ctx.componentMod.preloadFont( + href, + type, + ctx.renderOpts.crossOrigin, + ctx.nonce + ) } } else { try { let url = new URL(ctx.assetPrefix) - ctx.componentMod.preconnect(url.origin, 'anonymous') + ctx.componentMod.preconnect(url.origin, 'anonymous', ctx.nonce) } catch (error) { // assetPrefix must not be a fully qualified domain name. We assume // we should preconnect to same origin instead - ctx.componentMod.preconnect('/', 'anonymous') + ctx.componentMod.preconnect('/', 'anonymous', ctx.nonce) } } } @@ -78,7 +83,11 @@ export function getLayerAssets({ const precedence = process.env.NODE_ENV === 'development' ? 'next_' + href : 'next' - ctx.componentMod.preloadStyle(fullHref, ctx.renderOpts.crossOrigin) + ctx.componentMod.preloadStyle( + fullHref, + ctx.renderOpts.crossOrigin, + ctx.nonce + ) return ( ) }) @@ -99,7 +109,14 @@ export function getLayerAssets({ href )}${getAssetQueryString(ctx, true)}` - return + return ( + + ) }) : [] diff --git a/packages/next/src/server/app-render/rsc/preloads.ts b/packages/next/src/server/app-render/rsc/preloads.ts index fdb2bc39bdcc0..c7c11325edb97 100644 --- a/packages/next/src/server/app-render/rsc/preloads.ts +++ b/packages/next/src/server/app-render/rsc/preloads.ts @@ -6,29 +6,48 @@ Files in the rsc directory are meant to be packaged as part of the RSC graph usi import ReactDOM from 'react-dom' -export function preloadStyle(href: string, crossOrigin?: string | undefined) { +export function preloadStyle( + href: string, + crossOrigin: string | undefined, + nonce: string | undefined +) { const opts: any = { as: 'style' } if (typeof crossOrigin === 'string') { opts.crossOrigin = crossOrigin } + if (typeof nonce === 'string') { + opts.nonce = nonce + } ReactDOM.preload(href, opts) } export function preloadFont( href: string, type: string, - crossOrigin?: string | undefined + crossOrigin: string | undefined, + nonce: string | undefined ) { const opts: any = { as: 'font', type } if (typeof crossOrigin === 'string') { opts.crossOrigin = crossOrigin } + if (typeof nonce === 'string') { + opts.nonce = nonce + } ReactDOM.preload(href, opts) } -export function preconnect(href: string, crossOrigin?: string | undefined) { - ;(ReactDOM as any).preconnect( - href, - typeof crossOrigin === 'string' ? { crossOrigin } : undefined - ) +export function preconnect( + href: string, + crossOrigin: string | undefined, + nonce: string | undefined +) { + const opts: any = {} + if (typeof crossOrigin === 'string') { + opts.crossOrigin = crossOrigin + } + if (typeof nonce === 'string') { + opts.nonce = nonce + } + ;(ReactDOM as any).preconnect(href, opts) } diff --git a/test/e2e/app-dir/app/app/script-nonce/with-next-font/page.js b/test/e2e/app-dir/app/app/script-nonce/with-next-font/page.js new file mode 100644 index 0000000000000..7eb6d7898292d --- /dev/null +++ b/test/e2e/app-dir/app/app/script-nonce/with-next-font/page.js @@ -0,0 +1,11 @@ +import { Inter } from 'next/font/google' + +const inter = Inter({ subsets: ['latin'] }) + +export default function Page() { + return ( + <> +
script-nonce
+ > + ) +} diff --git a/test/e2e/app-dir/app/index.test.ts b/test/e2e/app-dir/app/index.test.ts index 5036662ba80aa..63762c179520d 100644 --- a/test/e2e/app-dir/app/index.test.ts +++ b/test/e2e/app-dir/app/index.test.ts @@ -1700,6 +1700,16 @@ describe('app dir - basic', () => { }) } }) + + it('should pass nonce when using next/font', async () => { + const html = await next.render('/script-nonce/with-next-font') + const $ = cheerio.load(html) + const scripts = $('script, link[rel="preload"][as="script"]') + + scripts.each((_, element) => { + expect(element.attribs.nonce).toBeTruthy() + }) + }) }) describe('data fetch with response over 16KB with chunked encoding', () => { diff --git a/test/e2e/app-dir/app/middleware.js b/test/e2e/app-dir/app/middleware.js index 51cbc66e29589..56bc894fc458a 100644 --- a/test/e2e/app-dir/app/middleware.js +++ b/test/e2e/app-dir/app/middleware.js @@ -78,4 +78,14 @@ export async function middleware(request) { }, }) } + + if (request.nextUrl.pathname === '/script-nonce/with-next-font') { + const nonce = crypto.randomUUID() + + return NextResponse.next({ + headers: { + 'content-security-policy': `script-src 'nonce-${nonce}' 'strict-dynamic';`, + }, + }) + } } diff --git a/test/turbopack-build-tests-manifest.json b/test/turbopack-build-tests-manifest.json index 90608e8e84416..2c15dbe3ec15a 100644 --- a/test/turbopack-build-tests-manifest.json +++ b/test/turbopack-build-tests-manifest.json @@ -1091,6 +1091,7 @@ "app dir - basic next/script should insert preload tags for beforeInteractive and afterInteractive scripts", "app dir - basic next/script should load stylesheets for next/scripts", "app dir - basic next/script should pass `nonce`", + "app dir - basic next/script should pass nonce when using next/font", "app dir - basic next/script should pass on extra props for beforeInteractive scripts with a src prop", "app dir - basic next/script should pass on extra props for beforeInteractive scripts without a src prop", "app dir - basic next/script should support next/script and render in correct order",