diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 2b26e47395a71..f4fae4d307efb 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -130,6 +130,9 @@ export type ResponseState = { startInlineScript: PrecomputedChunk, instructions: InstructionState, + // state for outputting CSP nonce + nonce: string | void, + // state for data streaming format externalRuntimeConfig: BootstrapScriptDescriptor | null, @@ -161,6 +164,7 @@ const endInlineScript = stringToPrecomputedChunk(''); const startScriptSrc = stringToPrecomputedChunk(''); @@ -245,10 +249,17 @@ export function createResponseState( typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src; const integrity = typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity; + bootstrapChunks.push( startScriptSrc, stringToChunk(escapeTextForBrowser(src)), ); + if (nonce) { + bootstrapChunks.push( + scriptNonce, + stringToChunk(escapeTextForBrowser(nonce)), + ); + } if (integrity) { bootstrapChunks.push( scriptIntegirty, @@ -265,10 +276,18 @@ export function createResponseState( typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src; const integrity = typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity; + bootstrapChunks.push( startModuleSrc, stringToChunk(escapeTextForBrowser(src)), ); + + if (nonce) { + bootstrapChunks.push( + scriptNonce, + stringToChunk(escapeTextForBrowser(nonce)), + ); + } if (integrity) { bootstrapChunks.push( scriptIntegirty, @@ -297,6 +316,7 @@ export function createResponseState( preloadChunks: [], hoistableChunks: [], stylesToHoist: false, + nonce, }; } @@ -4066,7 +4086,7 @@ export function writePreamble( // (User code could choose to send this even earlier by calling // preinit(...), if they know they will suspend). const {src, integrity} = responseState.externalRuntimeConfig; - internalPreinitScript(resources, src, integrity); + internalPreinitScript(resources, src, integrity, responseState.nonce); } const htmlChunks = responseState.htmlChunks; @@ -5349,6 +5369,7 @@ function internalPreinitScript( resources: Resources, src: string, integrity: ?string, + nonce: ?string, ): void { const key = getResourceKey('script', src); let resource = resources.scriptsMap.get(key); @@ -5365,6 +5386,7 @@ function internalPreinitScript( async: true, src, integrity, + nonce, }); } return; diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js index 474921d69a5e5..bc2e93706c828 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js @@ -57,6 +57,7 @@ export type ResponseState = { preloadChunks: Array, hoistableChunks: Array, stylesToHoist: boolean, + nonce: string | void, // This is an extra field for the legacy renderer generateStaticMarkup: boolean, }; @@ -94,6 +95,7 @@ export function createResponseState( preloadChunks: responseState.preloadChunks, hoistableChunks: responseState.hoistableChunks, stylesToHoist: responseState.stylesToHoist, + nonce: responseState.nonce, // This is an extra field for the legacy renderer generateStaticMarkup, diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 8d30c29ed74bd..1ba04fb0446d0 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -574,7 +574,7 @@ describe('ReactDOMFizzServer', () => { ); }); - it('should support nonce scripts', async () => { + it('should support nonce for bootstrap and runtime scripts', async () => { CSPnonce = 'R4nd0m'; try { let resolve; @@ -591,11 +591,26 @@ describe('ReactDOMFizzServer', () => { , - {nonce: 'R4nd0m'}, + { + nonce: 'R4nd0m', + bootstrapScriptContent: 'function noop(){}', + bootstrapScripts: ['init.js'], + bootstrapModules: ['init.mjs'], + }, ); pipe(writable); }); + expect(getVisibleChildren(container)).toEqual(
Loading...
); + + // check that there are 4 scripts with a matching nonce: + // The runtime script, an inline bootstrap script, and two src scripts + expect( + Array.from(container.getElementsByTagName('script')).filter( + node => node.getAttribute('nonce') === CSPnonce, + ).length, + ).toEqual(4); + await act(() => { resolve({default: Text}); }); @@ -605,6 +620,53 @@ describe('ReactDOMFizzServer', () => { } }); + it('should not automatically add nonce to rendered scripts', async () => { + CSPnonce = 'R4nd0m'; + try { + await act(async () => { + const {pipe} = renderToPipeableStream( + + + + `, + ``, + ``, + ``, + ``, + ``, + ``, + ]); + } finally { + CSPnonce = null; + } + }); + it('should client render a boundary if a lazy component rejects', async () => { let rejectComponent; const LazyComponent = React.lazy(() => { diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js index f73efe02b987d..c6dbae0d0c403 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js @@ -486,4 +486,21 @@ describe('ReactDOMFizzServerBrowser', () => { 'foobar', ); }); + + it('should support nonce attribute for bootstrap scripts', async () => { + const nonce = 'R4nd0m'; + const stream = await ReactDOMFizzServer.renderToReadableStream( +
hello world
, + { + nonce, + bootstrapScriptContent: 'INIT();', + bootstrapScripts: ['init.js'], + bootstrapModules: ['init.mjs'], + }, + ); + const result = await readResult(stream); + expect(result).toMatchInlineSnapshot( + `"
hello world
"`, + ); + }); }); diff --git a/packages/react-dom/src/test-utils/FizzTestUtils.js b/packages/react-dom/src/test-utils/FizzTestUtils.js index 1407504fd2b7f..a10bec7fa0dee 100644 --- a/packages/react-dom/src/test-utils/FizzTestUtils.js +++ b/packages/react-dom/src/test-utils/FizzTestUtils.js @@ -103,6 +103,12 @@ async function executeScript(script: Element) { } else { const newScript = ownerDocument.createElement('script'); newScript.textContent = script.textContent; + // make sure to add nonce back to script if it exists + const scriptNonce = script.getAttribute('nonce'); + if (scriptNonce) { + newScript.setAttribute('nonce', scriptNonce); + } + parent.insertBefore(newScript, script); parent.removeChild(script); }