mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
## What? Includes the changes from 16-2. --------- Co-authored-by: JJ Kasper <[email protected]> Co-authored-by: Josh Story <[email protected]> Co-authored-by: Zack Tanner <[email protected]> Co-authored-by: Benjamin Woodruff <[email protected]>
251 lines
9.3 KiB
TypeScript
251 lines
9.3 KiB
TypeScript
import { nextTestSetup } from 'e2e-utils'
|
|
import crypto from 'crypto'
|
|
import path from 'path'
|
|
import cheerio from 'cheerio'
|
|
import { getClientReferenceManifest } from 'next-test-utils'
|
|
|
|
describe('Subresource Integrity', () => {
|
|
describe.each(['node', 'edge', 'pages'] as const)(
|
|
'with %s runtime',
|
|
(runtime) => {
|
|
const { next } = nextTestSetup({
|
|
files: path.join(__dirname, 'fixture'),
|
|
})
|
|
|
|
function fetchWithPolicy(policy: string | null, reportOnly?: boolean) {
|
|
const cspKey = reportOnly
|
|
? 'Content-Security-Policy-Report-Only'
|
|
: 'Content-Security-Policy'
|
|
return next.fetch(`/${runtime}`, {
|
|
headers: policy
|
|
? {
|
|
[cspKey]: policy,
|
|
}
|
|
: {},
|
|
})
|
|
}
|
|
|
|
async function renderWithPolicy(
|
|
policy: string | null,
|
|
reportOnly?: boolean
|
|
) {
|
|
const res = await fetchWithPolicy(policy, reportOnly)
|
|
|
|
expect(res.ok).toBe(true)
|
|
|
|
const html = await res.text()
|
|
|
|
return cheerio.load(html)
|
|
}
|
|
|
|
it('does not include nonce when not enabled', async () => {
|
|
const policies = [
|
|
`script-src 'nonce-'`, // invalid nonce
|
|
'style-src "nonce-cmFuZG9tCg=="', // no script or default src
|
|
`script-src 'nonce-" onerror="alert(1)'`, // malformed nonce
|
|
'', // empty string
|
|
]
|
|
|
|
for (const policy of policies) {
|
|
const $ = await renderWithPolicy(policy)
|
|
|
|
// Find all the script tags without src attributes and with nonce
|
|
// attributes.
|
|
const elements = $('script[nonce]:not([src])')
|
|
|
|
// Expect there to be none.
|
|
expect(elements.length).toBe(0)
|
|
}
|
|
})
|
|
|
|
it('includes a nonce value with inline scripts when Content-Security-Policy 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)
|
|
|
|
// 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 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=='
|
|
|
|
// 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)
|
|
|
|
// Find all the script tags without src attributes.
|
|
const elements = $('script[src]')
|
|
|
|
// Expect there to be at least 2 script tag with a src attribute.
|
|
// The main chunk and the webpack runtime.
|
|
expect(elements.length).toBeGreaterThan(1)
|
|
|
|
// Expect all inline scripts to have the nonce value.
|
|
elements.each((i, el) => {
|
|
expect(el.attribs['nonce']).toBe(nonce)
|
|
})
|
|
}
|
|
})
|
|
|
|
it('includes an integrity attribute on scripts', async () => {
|
|
// pages router doesn't do integrity attribute yet
|
|
if (runtime === 'pages') return
|
|
|
|
const $ = await next.render$(`/${runtime}`)
|
|
// Currently webpack chunks loaded via flight runtime do not get integrity
|
|
// hashes. This was previously unobservable in this test because these scripts
|
|
// are inserted by the webpack runtime and immediately removed from the document.
|
|
// However with the advent of preinitialization of chunks used during SSR there are
|
|
// some script tags for flight loaded chunks that will be part of the initial HTML
|
|
// but do not have integrity hashes. Flight does not currently support a way to
|
|
// provide integrity hashes for these chunks. When this is addressed in React upstream
|
|
// we can revisit this tests assertions and start to ensure it actually applies to
|
|
// all SSR'd scripts. For now we will look for known entrypoint scripts and assume
|
|
// everything else in the <head> is part of flight loaded chunks
|
|
|
|
// Collect all the scripts with integrity hashes so we can verify them.
|
|
const files: Map<string, string> = new Map()
|
|
|
|
function collectFile(el: CheerioElement) {
|
|
const integrity = el.attribs['integrity']
|
|
const src = el.attribs['src']
|
|
expect(src).toBeDefined()
|
|
files.set(src, integrity)
|
|
}
|
|
|
|
const clientReferenceManifest = getClientReferenceManifest(
|
|
next,
|
|
`/${runtime}/page`
|
|
)
|
|
const clientReferenceChunks = new Set(
|
|
Object.values(clientReferenceManifest.clientModules).flatMap(
|
|
(clientModule) =>
|
|
clientModule.chunks
|
|
.filter((c) => c.includes('.js'))
|
|
.map(
|
|
(c) =>
|
|
new URL(
|
|
clientReferenceManifest.moduleLoading.prefix + c,
|
|
'http://localhost'
|
|
).pathname
|
|
)
|
|
)
|
|
)
|
|
|
|
const headScripts = $('head script[src]')
|
|
expect(headScripts.length).toBeGreaterThan(0)
|
|
headScripts.each((i, el) => {
|
|
collectFile(el)
|
|
})
|
|
|
|
const bodyScripts = $('body script[src]')
|
|
expect(bodyScripts.length).toBeGreaterThan(0)
|
|
bodyScripts.each((i, el) => {
|
|
collectFile(el)
|
|
})
|
|
|
|
// For each script tag, ensure that the integrity attribute is the
|
|
// correct hash of the script tag.
|
|
let scriptsWithIntegrity = 0
|
|
for (const [src, integrity] of files) {
|
|
const res = await next.fetch(src)
|
|
expect(res.status).toBe(200)
|
|
const content = await res.text()
|
|
|
|
if (clientReferenceChunks.has(new URL(src, next.url).pathname)) {
|
|
// this is a flight loaded script, which currently do not have integrity hashes
|
|
continue
|
|
}
|
|
scriptsWithIntegrity++
|
|
|
|
const hash = crypto
|
|
.createHash('sha256')
|
|
.update(content)
|
|
.digest()
|
|
.toString('base64')
|
|
|
|
expect(integrity).toBe(`sha256-${hash}`)
|
|
}
|
|
|
|
// A pretty arbitrary number
|
|
expect(scriptsWithIntegrity).toBeGreaterThanOrEqual(2)
|
|
})
|
|
|
|
it('ignores malformed nonce values without failing the request', async () => {
|
|
const policies = [
|
|
`script-src 'nonce-"><script></script>"'`,
|
|
`script-src 'nonce-" onerror="alert(1)'`,
|
|
]
|
|
|
|
for (const policy of policies) {
|
|
const $ = await renderWithPolicy(policy)
|
|
|
|
expect($('script[nonce]').length).toBe(0)
|
|
}
|
|
})
|
|
}
|
|
)
|
|
})
|