@samitouri / QOS-React-2 / commits / 3677c019af

Support nonce option to be passed to inline scripts (#22593)

Sebastian Markbåge committed Oct 19, 2021 at 21:14 UTC 3677c019af7251207dee41df7689859ab991a665
5 files changed +70 -12
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+37 -1
@@ -23,6 +23,7 @@ let PropTypes;
23 let textCache;
24 let document;
25 let writable;
26 +let CSPnonce = null;
27 let container;
28 let buffer = '';
29 let hasErrored = false;
@@ -91,7 +92,10 @@ describe('ReactDOMFizzServer', () => {
92 fakeBody.innerHTML = bufferedContent;
93 while (fakeBody.firstChild) {
94 const node = fakeBody.firstChild;
94 - if (node.nodeName === 'SCRIPT') {
95 + if (
96 + node.nodeName === 'SCRIPT' &&
97 + (CSPnonce === null || node.getAttribute('nonce') === CSPnonce)
98 + ) {
99 const script = document.createElement('script');
100 script.textContent = node.textContent;
101 fakeBody.removeChild(node);
@@ -281,6 +285,38 @@ describe('ReactDOMFizzServer', () => {
285 );
286 });
287
288 + // @gate experimental
289 + it('should support nonce scripts', async () => {
290 + CSPnonce = 'R4nd0m';
291 + try {
292 + let resolve;
293 + const Lazy = React.lazy(() => {
294 + return new Promise(r => {
295 + resolve = r;
296 + });
297 + });
298 +
299 + await act(async () => {
300 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
301 + <div>
302 + <Suspense fallback={<Text text="Loading..." />}>
303 + <Lazy text="Hello" />
304 + </Suspense>
305 + </div>,
306 + {nonce: 'R4nd0m'},
307 + );
308 + pipe(writable);
309 + });
310 + expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
311 + await act(async () => {
312 + resolve({default: Text});
313 + });
314 + expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
315 + } finally {
316 + CSPnonce = null;
317 + }
318 + });
319 +
320 // @gate experimental
321 it('should client render a boundary if a lazy component rejects', async () => {
322 let rejectComponent;
packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
+5 -1
@@ -26,6 +26,7 @@ import {
26 type Options = {|
27 identifierPrefix?: string,
28 namespaceURI?: string,
29 + nonce?: string,
30 progressiveChunkSize?: number,
31 signal?: AbortSignal,
32 onCompleteShell?: () => void,
@@ -39,7 +40,10 @@ function renderToReadableStream(
40 ): ReadableStream {
41 const request = createRequest(
42 children,
42 - createResponseState(options ? options.identifierPrefix : undefined),
43 + createResponseState(
44 + options ? options.identifierPrefix : undefined,
45 + options ? options.nonce : undefined,
46 + ),
47 createRootFormatContext(options ? options.namespaceURI : undefined),
48 options ? options.progressiveChunkSize : undefined,
49 options ? options.onError : undefined,
packages/react-dom/src/server/ReactDOMFizzServerNode.js
+5 -1
@@ -31,6 +31,7 @@ function createDrainHandler(destination, request) {
31 type Options = {|
32 identifierPrefix?: string,
33 namespaceURI?: string,
34 + nonce?: string,
35 progressiveChunkSize?: number,
36 onCompleteShell?: () => void,
37 onCompleteAll?: () => void,
@@ -47,7 +48,10 @@ type Controls = {|
48 function createRequestImpl(children: ReactNodeList, options: void | Options) {
49 return createRequest(
50 children,
50 - createResponseState(options ? options.identifierPrefix : undefined),
51 + createResponseState(
52 + options ? options.identifierPrefix : undefined,
53 + options ? options.nonce : undefined,
54 + ),
55 createRootFormatContext(options ? options.namespaceURI : undefined),
56 options ? options.progressiveChunkSize : undefined,
57 options ? options.onError : undefined,
packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+20 -8
@@ -59,6 +59,7 @@ export const isPrimaryRenderer = true;
59
60 // Per response, global state that is not contextual to the rendering subtree.
61 export type ResponseState = {
62 + startInlineScript: PrecomputedChunk,
63 placeholderPrefix: PrecomputedChunk,
64 segmentPrefix: PrecomputedChunk,
65 boundaryPrefix: string,
@@ -71,12 +72,22 @@ export type ResponseState = {
72 ...
73 };
74
75 +const startInlineScript = stringToPrecomputedChunk('<script>');
76 +
77 // Allows us to keep track of what we've already written so we can refer back to it.
78 export function createResponseState(
79 identifierPrefix: string | void,
80 + nonce: string | void,
81 ): ResponseState {
82 const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;
83 + const inlineScriptWithNonce =
84 + nonce === undefined
85 + ? startInlineScript
86 + : stringToPrecomputedChunk(
87 + '<script nonce="' + escapeTextForBrowser(nonce) + '">',
88 + );
89 return {
90 + startInlineScript: inlineScriptWithNonce,
91 placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),
92 segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),
93 boundaryPrefix: idPrefix + 'B:',
@@ -1689,9 +1700,9 @@ const clientRenderFunction =
1700 'function $RX(a){if(a=document.getElementById(a))a=a.previousSibling,a.data="$!",a._reactRetry&&a._reactRetry()}';
1701
1702 const completeSegmentScript1Full = stringToPrecomputedChunk(
1692 - '<script>' + completeSegmentFunction + ';$RS("',
1703 + completeSegmentFunction + ';$RS("',
1704 );
1694 -const completeSegmentScript1Partial = stringToPrecomputedChunk('<script>$RS("');
1705 +const completeSegmentScript1Partial = stringToPrecomputedChunk('$RS("');
1706 const completeSegmentScript2 = stringToPrecomputedChunk('","');
1707 const completeSegmentScript3 = stringToPrecomputedChunk('")</script>');
1708
@@ -1700,6 +1711,7 @@ export function writeCompletedSegmentInstruction(
1711 responseState: ResponseState,
1712 contentSegmentID: number,
1713 ): boolean {
1714 + writeChunk(destination, responseState.startInlineScript);
1715 if (!responseState.sentCompleteSegmentFunction) {
1716 // The first time we write this, we'll need to include the full implementation.
1717 responseState.sentCompleteSegmentFunction = true;
@@ -1718,11 +1730,9 @@ export function writeCompletedSegmentInstruction(
1730 }
1731
1732 const completeBoundaryScript1Full = stringToPrecomputedChunk(
1721 - '<script>' + completeBoundaryFunction + ';$RC("',
1722 -);
1723 -const completeBoundaryScript1Partial = stringToPrecomputedChunk(
1724 - '<script>$RC("',
1733 + completeBoundaryFunction + ';$RC("',
1734 );
1735 +const completeBoundaryScript1Partial = stringToPrecomputedChunk('$RC("');
1736 const completeBoundaryScript2 = stringToPrecomputedChunk('","');
1737 const completeBoundaryScript3 = stringToPrecomputedChunk('")</script>');
1738
@@ -1732,6 +1742,7 @@ export function writeCompletedBoundaryInstruction(
1742 boundaryID: SuspenseBoundaryID,
1743 contentSegmentID: number,
1744 ): boolean {
1745 + writeChunk(destination, responseState.startInlineScript);
1746 if (!responseState.sentCompleteBoundaryFunction) {
1747 // The first time we write this, we'll need to include the full implementation.
1748 responseState.sentCompleteBoundaryFunction = true;
@@ -1756,9 +1767,9 @@ export function writeCompletedBoundaryInstruction(
1767 }
1768
1769 const clientRenderScript1Full = stringToPrecomputedChunk(
1759 - '<script>' + clientRenderFunction + ';$RX("',
1770 + clientRenderFunction + ';$RX("',
1771 );
1761 -const clientRenderScript1Partial = stringToPrecomputedChunk('<script>$RX("');
1772 +const clientRenderScript1Partial = stringToPrecomputedChunk('$RX("');
1773 const clientRenderScript2 = stringToPrecomputedChunk('")</script>');
1774
1775 export function writeClientRenderBoundaryInstruction(
@@ -1766,6 +1777,7 @@ export function writeClientRenderBoundaryInstruction(
1777 responseState: ResponseState,
1778 boundaryID: SuspenseBoundaryID,
1779 ): boolean {
1780 + writeChunk(destination, responseState.startInlineScript);
1781 if (!responseState.sentClientRenderFunction) {
1782 // The first time we write this, we'll need to include the full implementation.
1783 responseState.sentClientRenderFunction = true;
packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js
+3 -1
@@ -29,6 +29,7 @@ export const isPrimaryRenderer = false;
29
30 export type ResponseState = {
31 // Keep this in sync with ReactDOMServerFormatConfig
32 + startInlineScript: PrecomputedChunk,
33 placeholderPrefix: PrecomputedChunk,
34 segmentPrefix: PrecomputedChunk,
35 boundaryPrefix: string,
@@ -46,9 +47,10 @@ export function createResponseState(
47 generateStaticMarkup: boolean,
48 identifierPrefix: string | void,
49 ): ResponseState {
49 - const responseState = createResponseStateImpl(identifierPrefix);
50 + const responseState = createResponseStateImpl(identifierPrefix, undefined);
51 return {
52 // Keep this in sync with ReactDOMServerFormatConfig
53 + startInlineScript: responseState.startInlineScript,
54 placeholderPrefix: responseState.placeholderPrefix,
55 segmentPrefix: responseState.segmentPrefix,
56 boundaryPrefix: responseState.boundaryPrefix,