Skip to content

Commit

Permalink
Support adding CSP nonce with content-security-policy-report-only h…
Browse files Browse the repository at this point in the history
…eader (#59071)

**Note**: this is a 1-to-1 copy of #48969 by @danieltott with all the
merge conflicts fixed.

## Checklist

* Fixes #48966
* Tests added to
`test/production/app-dir/subresource-integrity/subresource-integrity.test.ts`

## Description

Currently `renderToHTMLOrFlight` in app-render pulls out a nonce value
from a `content-security-policy` header for use in generating script
tags:


https://github.com/vercel/next.js/blob/e7c9d3c051e6027cf187e0d70565417d6037e37c/packages/next/src/server/app-render/app-render.tsx#L1204

That misses the ability to use a [content-security-policy-report-only
header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only).
Many times this is a required step to enabling a CSP - by shipping a CSP
with report-only and collecting reports before actually blocking
resources.

## Changes

* Added ability to check `content-security-policy-report-only` header in
`renderToHTMLOrFlight()`
* Added test to verify `nonce` is correctly applied when
`content-security-policy-report-only` header exists

Co-authored-by: Dan Ott <[email protected]>
Co-authored-by: Zack Tanner <[email protected]>
  • Loading branch information
3 people authored Dec 1, 2023
1 parent cdff6df commit 7458ffa
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 5 deletions.
4 changes: 3 additions & 1 deletion packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,9 @@ async function renderToHTMLOrFlightImpl(
: null

// Get the nonce from the incoming request if it has one.
const csp = req.headers['content-security-policy']
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,24 @@ createNextDescribe(
files: path.join(__dirname, 'fixture'),
},
({ next }) => {
function fetchWithPolicy(policy: string | null) {
function fetchWithPolicy(policy: string | null, reportOnly?: boolean) {
const cspKey = reportOnly
? 'Content-Security-Policy-Report-Only'
: 'Content-Security-Policy'
return next.fetch('/dashboard', {
headers: policy
? {
'Content-Security-Policy': policy,
[cspKey]: policy,
}
: {},
})
}

async function renderWithPolicy(policy: string | null) {
const res = await fetchWithPolicy(policy)
async function renderWithPolicy(
policy: string | null,
reportOnly?: boolean
) {
const res = await fetchWithPolicy(policy, reportOnly)

expect(res.ok).toBe(true)

Expand Down Expand Up @@ -78,6 +84,36 @@ createNextDescribe(
}
})

it('includes a nonce value with inline scripts when Content-Security-Policy-Report-Only header is defined', async () => {
// A random nonce value, base64 encoded.
const nonce = 'cmFuZG9tCg=='

// Validate all the cases where we could parse the nonce.
const policies = [
`script-src 'nonce-${nonce}'`, // base case
` script-src 'nonce-${nonce}' `, // extra space added around sources and directive
`style-src 'self'; script-src 'nonce-${nonce}'`, // extra directives
`script-src 'self' 'nonce-${nonce}' 'nonce-othernonce'`, // extra nonces
`default-src 'nonce-othernonce'; script-src 'nonce-${nonce}';`, // script and then fallback case
`default-src 'nonce-${nonce}'`, // fallback case
]

for (const policy of policies) {
const $ = await renderWithPolicy(policy, true)

// Find all the script tags without src attributes.
const elements = $('script:not([src])')

// Expect there to be at least 1 script tag without a src attribute.
expect(elements.length).toBeGreaterThan(0)

// Expect all inline scripts to have the nonce value.
elements.each((i, el) => {
expect(el.attribs['nonce']).toBe(nonce)
})
}
})

it('includes a nonce value with bootstrap scripts when Content-Security-Policy header is defined', async () => {
// A random nonce value, base64 encoded.
const nonce = 'cmFuZG9tCg=='
Expand Down

0 comments on commit 7458ffa

Please sign in to comment.