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,