Skip to content

Commit

Permalink
Handle nonce on Next.js injected script/link tags (#65508)
Browse files Browse the repository at this point in the history
## What

Ensures `nonce` is added to script and link tags Next.js renders.
Additional cases it now handles:

- We already passed `nonce` to the React rendering, though not
consistently on all cases where `renderToStream` is called, I'm
surprised there haven't been more reports of this, but now it will pass
it on all cases where React rendering is called that I could find
- In `get-layer-assets.tsx` we now pass `nonce` to both the `script` and
`link` tags
- When calling `ReactDOM.preload` the nonce was missing as well, ensured
that the nonce is included in that case as well.

Added a test that mimicks the reproduction by adding `next/font` in this
case.

Fixes #64037
Closes PACK-2973  

<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:

## For Contributors

### Improving Documentation

- Run `pnpm prettier-fix` to fix formatting issues before opening the
PR.
- Read the Docs Contribution Guide to ensure your contribution follows
the docs guidelines:
https://nextjs.org/docs/community/contribution-guide

### Adding or Updating Examples

- The "examples guidelines" are followed from our contributing doc
https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md
- Make sure the linting passes by running `pnpm build && pnpm lint`. See
https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md

### Fixing a bug

- Related issues linked using `fixes #number`
- Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md

### Adding a feature

- Implements an existing feature request or RFC. Make sure the feature
request has been accepted for implementation before opening a PR. (A
discussion must be opened, see
https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added
(https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md


## For Maintainers

- Minimal description (aim for explaining to someone not on the team to
understand the PR)
- When linking to a Slack thread, you might want to share details of the
conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Closes NEXT-
Fixes #

-->
  • Loading branch information
timneutkens committed May 10, 2024
1 parent ac55c79 commit 8c5add2
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 21 deletions.
23 changes: 14 additions & 9 deletions packages/next/src/server/app-render/app-render.tsx
Expand Up @@ -145,6 +145,7 @@ export type AppRenderContext = AppRenderBaseContext & {
flightDataRendererErrorHandler: ErrorHandler
serverComponentsErrorHandler: ErrorHandler
isNotFoundPath: boolean
nonce: string | undefined
res: BaseNextResponse
}

Expand Down Expand Up @@ -362,6 +363,7 @@ async function generateFlight(
ctx.clientReferenceManifest.clientModules,
{
onError: ctx.flightDataRendererErrorHandler,
nonce: ctx.nonce,
}
)

Expand Down Expand Up @@ -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,
Expand All @@ -859,6 +870,7 @@ async function renderToHTMLOrFlightImpl(
flightDataRendererErrorHandler,
serverComponentsErrorHandler,
isNotFoundPath,
nonce,
res,
}

Expand All @@ -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 } =
Expand Down Expand Up @@ -943,6 +946,7 @@ async function renderToHTMLOrFlightImpl(
clientReferenceManifest.clientModules,
{
onError: serverComponentsErrorHandler,
nonce,
}
)

Expand Down Expand Up @@ -1279,6 +1283,7 @@ async function renderToHTMLOrFlightImpl(
clientReferenceManifest.clientModules,
{
onError: serverComponentsErrorHandler,
nonce,
}
)

Expand Down
27 changes: 22 additions & 5 deletions packages/next/src/server/app-render/get-layer-assets.tsx
Expand Up @@ -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)
}
}
}
Expand All @@ -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 (
<link
Expand All @@ -88,6 +97,7 @@ export function getLayerAssets({
precedence={precedence}
crossOrigin={ctx.renderOpts.crossOrigin}
key={index}
nonce={ctx.nonce}
/>
)
})
Expand All @@ -99,7 +109,14 @@ export function getLayerAssets({
href
)}${getAssetQueryString(ctx, true)}`

return <script src={fullSrc} async={true} key={`script-${index}`} />
return (
<script
src={fullSrc}
async={true}
key={`script-${index}`}
nonce={ctx.nonce}
/>
)
})
: []

Expand Down
33 changes: 26 additions & 7 deletions packages/next/src/server/app-render/rsc/preloads.ts
Expand Up @@ -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)
}
11 changes: 11 additions & 0 deletions 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 (
<>
<p className={inter.className}>script-nonce</p>
</>
)
}
10 changes: 10 additions & 0 deletions test/e2e/app-dir/app/index.test.ts
Expand Up @@ -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', () => {
Expand Down
10 changes: 10 additions & 0 deletions test/e2e/app-dir/app/middleware.js
Expand Up @@ -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';`,
},
})
}
}
1 change: 1 addition & 0 deletions test/turbopack-build-tests-manifest.json
Expand Up @@ -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",
Expand Down

0 comments on commit 8c5add2

Please sign in to comment.