From 95af0fdc07fc02419c2fa4aba077da274e609533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 19 Oct 2021 21:14:26 -0400 Subject: [PATCH] Support nonce option to be passed to inline scripts (#22593) --- .../src/__tests__/ReactDOMFizzServer-test.js | 38 ++++++++++++++++++- .../src/server/ReactDOMFizzServerBrowser.js | 6 ++- .../src/server/ReactDOMFizzServerNode.js | 6 ++- .../src/server/ReactDOMServerFormatConfig.js | 28 ++++++++++---- .../ReactDOMServerLegacyFormatConfig.js | 4 +- 5 files changed, 70 insertions(+), 12 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index cd0c87a1df3b2..ec3a0bfadaee4 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -23,6 +23,7 @@ let PropTypes; let textCache; let document; let writable; +let CSPnonce = null; let container; let buffer = ''; let hasErrored = false; @@ -91,7 +92,10 @@ describe('ReactDOMFizzServer', () => { fakeBody.innerHTML = bufferedContent; while (fakeBody.firstChild) { const node = fakeBody.firstChild; - if (node.nodeName === 'SCRIPT') { + if ( + node.nodeName === 'SCRIPT' && + (CSPnonce === null || node.getAttribute('nonce') === CSPnonce) + ) { const script = document.createElement('script'); script.textContent = node.textContent; fakeBody.removeChild(node); @@ -281,6 +285,38 @@ describe('ReactDOMFizzServer', () => { ); }); + // @gate experimental + it('should support nonce scripts', async () => { + CSPnonce = 'R4nd0m'; + try { + let resolve; + const Lazy = React.lazy(() => { + return new Promise(r => { + resolve = r; + }); + }); + + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( +
+ }> + + +
, + {nonce: 'R4nd0m'}, + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual(
Loading...
); + await act(async () => { + resolve({default: Text}); + }); + expect(getVisibleChildren(container)).toEqual(
Hello
); + } finally { + CSPnonce = null; + } + }); + // @gate experimental it('should client render a boundary if a lazy component rejects', async () => { let rejectComponent; diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js index d238201247626..2865ef46b2574 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js @@ -26,6 +26,7 @@ import { type Options = {| identifierPrefix?: string, namespaceURI?: string, + nonce?: string, progressiveChunkSize?: number, signal?: AbortSignal, onCompleteShell?: () => void, @@ -39,7 +40,10 @@ function renderToReadableStream( ): ReadableStream { const request = createRequest( children, - createResponseState(options ? options.identifierPrefix : undefined), + createResponseState( + options ? options.identifierPrefix : undefined, + options ? options.nonce : undefined, + ), createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerNode.js b/packages/react-dom/src/server/ReactDOMFizzServerNode.js index 4233a88e1fb94..fe532a32c3b50 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerNode.js @@ -31,6 +31,7 @@ function createDrainHandler(destination, request) { type Options = {| identifierPrefix?: string, namespaceURI?: string, + nonce?: string, progressiveChunkSize?: number, onCompleteShell?: () => void, onCompleteAll?: () => void, @@ -47,7 +48,10 @@ type Controls = {| function createRequestImpl(children: ReactNodeList, options: void | Options) { return createRequest( children, - createResponseState(options ? options.identifierPrefix : undefined), + createResponseState( + options ? options.identifierPrefix : undefined, + options ? options.nonce : undefined, + ), createRootFormatContext(options ? options.namespaceURI : undefined), options ? options.progressiveChunkSize : undefined, options ? options.onError : undefined, diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js index 0c4b155bb881d..67d90f8513452 100644 --- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js @@ -59,6 +59,7 @@ export const isPrimaryRenderer = true; // Per response, global state that is not contextual to the rendering subtree. export type ResponseState = { + startInlineScript: PrecomputedChunk, placeholderPrefix: PrecomputedChunk, segmentPrefix: PrecomputedChunk, boundaryPrefix: string, @@ -71,12 +72,22 @@ export type ResponseState = { ... }; +const startInlineScript = stringToPrecomputedChunk(''); @@ -1700,6 +1711,7 @@ export function writeCompletedSegmentInstruction( responseState: ResponseState, contentSegmentID: number, ): boolean { + writeChunk(destination, responseState.startInlineScript); if (!responseState.sentCompleteSegmentFunction) { // The first time we write this, we'll need to include the full implementation. responseState.sentCompleteSegmentFunction = true; @@ -1718,11 +1730,9 @@ export function writeCompletedSegmentInstruction( } const completeBoundaryScript1Full = stringToPrecomputedChunk( - ''); @@ -1732,6 +1742,7 @@ export function writeCompletedBoundaryInstruction( boundaryID: SuspenseBoundaryID, contentSegmentID: number, ): boolean { + writeChunk(destination, responseState.startInlineScript); if (!responseState.sentCompleteBoundaryFunction) { // The first time we write this, we'll need to include the full implementation. responseState.sentCompleteBoundaryFunction = true; @@ -1756,9 +1767,9 @@ export function writeCompletedBoundaryInstruction( } const clientRenderScript1Full = stringToPrecomputedChunk( - ''); export function writeClientRenderBoundaryInstruction( @@ -1766,6 +1777,7 @@ export function writeClientRenderBoundaryInstruction( responseState: ResponseState, boundaryID: SuspenseBoundaryID, ): boolean { + writeChunk(destination, responseState.startInlineScript); if (!responseState.sentClientRenderFunction) { // The first time we write this, we'll need to include the full implementation. responseState.sentClientRenderFunction = true; diff --git a/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js index 59fb19e7986c8..723c03dfdba12 100644 --- a/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js +++ b/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js @@ -29,6 +29,7 @@ export const isPrimaryRenderer = false; export type ResponseState = { // Keep this in sync with ReactDOMServerFormatConfig + startInlineScript: PrecomputedChunk, placeholderPrefix: PrecomputedChunk, segmentPrefix: PrecomputedChunk, boundaryPrefix: string, @@ -46,9 +47,10 @@ export function createResponseState( generateStaticMarkup: boolean, identifierPrefix: string | void, ): ResponseState { - const responseState = createResponseStateImpl(identifierPrefix); + const responseState = createResponseStateImpl(identifierPrefix, undefined); return { // Keep this in sync with ReactDOMServerFormatConfig + startInlineScript: responseState.startInlineScript, placeholderPrefix: responseState.placeholderPrefix, segmentPrefix: responseState.segmentPrefix, boundaryPrefix: responseState.boundaryPrefix,