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(
+
+
+
+
+
+
+
+
+
+
+ ,
+ {
+ nonce: CSPnonce,
+ },
+ );
+ pipe(writable);
+ });
+
+ expect(
+ stripExternalRuntimeInNodes(
+ document.getElementsByTagName('script'),
+ renderOptions.unstable_externalRuntimeSrc,
+ ).map(n => n.outerHTML),
+ ).toEqual([
+ ``,
+ ``,
+ ``,
+ ``,
+ ``,
+ ``,
+ ``,
+ ]);
+ } 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);
}