@samitouri / QOS-React-2 / commits / 1e5245df89

support subresource integrity for bootstrapScripts and bootstrapModules (#25104)

Josh Story committed Aug 17, 2022 at 08:31 UTC 1e5245df89e45b788acf3af3769f0d816b18458d
6 files changed +101 -14
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+55
@@ -3390,6 +3390,61 @@ describe('ReactDOMFizzServer', () => {
3390 });
3391 });
3392
3393 + it('accepts an integrity property for bootstrapScripts and bootstrapModules', async () => {
3394 + await actIntoEmptyDocument(() => {
3395 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
3396 + <html>
3397 + <head />
3398 + <body>
3399 + <div>hello world</div>
3400 + </body>
3401 + </html>,
3402 + {
3403 + bootstrapScripts: [
3404 + 'foo',
3405 + {
3406 + src: 'bar',
3407 + },
3408 + {
3409 + src: 'baz',
3410 + integrity: 'qux',
3411 + },
3412 + ],
3413 + bootstrapModules: [
3414 + 'quux',
3415 + {
3416 + src: 'corge',
3417 + },
3418 + {
3419 + src: 'grault',
3420 + integrity: 'garply',
3421 + },
3422 + ],
3423 + },
3424 + );
3425 + pipe(writable);
3426 + });
3427 +
3428 + expect(getVisibleChildren(document)).toEqual(
3429 + <html>
3430 + <head />
3431 + <body>
3432 + <div>hello world</div>
3433 + </body>
3434 + </html>,
3435 + );
3436 + expect(
3437 + Array.from(document.getElementsByTagName('script')).map(n => n.outerHTML),
3438 + ).toEqual([
3439 + '<script src="foo" async=""></script>',
3440 + '<script src="bar" async=""></script>',
3441 + '<script src="baz" integrity="qux" async=""></script>',
3442 + '<script type="module" src="quux" async=""></script>',
3443 + '<script type="module" src="corge" async=""></script>',
3444 + '<script type="module" src="grault" integrity="garply" async=""></script>',
3445 + ]);
3446 + });
3447 +
3448 describe('bootstrapScriptContent escaping', () => {
3449 it('the "S" in "</?[Ss]cript" strings are replaced with unicode escaped lowercase s or S depending on case, preserving case sensitivity of nearby characters', async () => {
3450 window.__test_outlet = '';
packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
+3 -2
@@ -8,6 +8,7 @@
8 */
9
10 import type {ReactNodeList} from 'shared/ReactTypes';
11 +import type {BootstrapScriptDescriptor} from './ReactDOMServerFormatConfig';
12
13 import ReactVersion from 'shared/ReactVersion';
14
@@ -28,8 +29,8 @@ type Options = {|
29 namespaceURI?: string,
30 nonce?: string,
31 bootstrapScriptContent?: string,
31 - bootstrapScripts?: Array<string>,
32 - bootstrapModules?: Array<string>,
32 + bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
33 + bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
34 progressiveChunkSize?: number,
35 signal?: AbortSignal,
36 onError?: (error: mixed) => ?string,
packages/react-dom/src/server/ReactDOMFizzServerNode.js
+3 -2
@@ -9,6 +9,7 @@
9
10 import type {ReactNodeList} from 'shared/ReactTypes';
11 import type {Writable} from 'stream';
12 +import type {BootstrapScriptDescriptor} from './ReactDOMServerFormatConfig';
13
14 import ReactVersion from 'shared/ReactVersion';
15
@@ -38,8 +39,8 @@ type Options = {|
39 namespaceURI?: string,
40 nonce?: string,
41 bootstrapScriptContent?: string,
41 - bootstrapScripts?: Array<string>,
42 - bootstrapModules?: Array<string>,
42 + bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
43 + bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
44 progressiveChunkSize?: number,
45 onShellReady?: () => void,
46 onShellError?: (error: mixed) => void,
packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
+3 -2
@@ -8,6 +8,7 @@
8 */
9
10 import type {ReactNodeList} from 'shared/ReactTypes';
11 +import type {BootstrapScriptDescriptor} from './ReactDOMServerFormatConfig';
12
13 import ReactVersion from 'shared/ReactVersion';
14
@@ -27,8 +28,8 @@ type Options = {|
28 identifierPrefix?: string,
29 namespaceURI?: string,
30 bootstrapScriptContent?: string,
30 - bootstrapScripts?: Array<string>,
31 - bootstrapModules?: Array<string>,
31 + bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
32 + bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
33 progressiveChunkSize?: number,
34 signal?: AbortSignal,
35 onError?: (error: mixed) => ?string,
packages/react-dom/src/server/ReactDOMFizzStaticNode.js
+4 -2
@@ -8,6 +8,8 @@
8 */
9
10 import type {ReactNodeList} from 'shared/ReactTypes';
11 +import type {BootstrapScriptDescriptor} from './ReactDOMServerFormatConfig';
12 +
13 import {Writable, Readable} from 'stream';
14
15 import ReactVersion from 'shared/ReactVersion';
@@ -28,8 +30,8 @@ type Options = {|
30 identifierPrefix?: string,
31 namespaceURI?: string,
32 bootstrapScriptContent?: string,
31 - bootstrapScripts?: Array<string>,
32 - bootstrapModules?: Array<string>,
33 + bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
34 + bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
35 progressiveChunkSize?: number,
36 signal?: AbortSignal,
37 onError?: (error: mixed) => ?string,
packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+33 -6
@@ -82,6 +82,7 @@ const endInlineScript = stringToPrecomputedChunk('</script>');
82
83 const startScriptSrc = stringToPrecomputedChunk('<script src="');
84 const startModuleSrc = stringToPrecomputedChunk('<script type="module" src="');
85 +const scriptIntegirty = stringToPrecomputedChunk('" integrity="');
86 const endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
87
88 /**
@@ -104,13 +105,17 @@ const scriptRegex = /(<\/|<)(s)(cript)/gi;
105 const scriptReplacer = (match, prefix, s, suffix) =>
106 `${prefix}${s === 's' ? '\\u0073' : '\\u0053'}${suffix}`;
107
108 +export type BootstrapScriptDescriptor = {
109 + src: string,
110 + integrity?: string,
111 +};
112 // Allows us to keep track of what we've already written so we can refer back to it.
113 export function createResponseState(
114 identifierPrefix: string | void,
115 nonce: string | void,
116 bootstrapScriptContent: string | void,
112 - bootstrapScripts: Array<string> | void,
113 - bootstrapModules: Array<string> | void,
117 + bootstrapScripts: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,
118 + bootstrapModules: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,
119 ): ResponseState {
120 const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;
121 const inlineScriptWithNonce =
@@ -129,20 +134,42 @@ export function createResponseState(
134 }
135 if (bootstrapScripts !== undefined) {
136 for (let i = 0; i < bootstrapScripts.length; i++) {
137 + const scriptConfig = bootstrapScripts[i];
138 + const src =
139 + typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src;
140 + const integrity =
141 + typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;
142 bootstrapChunks.push(
143 startScriptSrc,
134 - stringToChunk(escapeTextForBrowser(bootstrapScripts[i])),
135 - endAsyncScript,
144 + stringToChunk(escapeTextForBrowser(src)),
145 );
146 + if (integrity) {
147 + bootstrapChunks.push(
148 + scriptIntegirty,
149 + stringToChunk(escapeTextForBrowser(integrity)),
150 + );
151 + }
152 + bootstrapChunks.push(endAsyncScript);
153 }
154 }
155 if (bootstrapModules !== undefined) {
156 for (let i = 0; i < bootstrapModules.length; i++) {
157 + const scriptConfig = bootstrapModules[i];
158 + const src =
159 + typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src;
160 + const integrity =
161 + typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;
162 bootstrapChunks.push(
163 startModuleSrc,
143 - stringToChunk(escapeTextForBrowser(bootstrapModules[i])),
144 - endAsyncScript,
164 + stringToChunk(escapeTextForBrowser(src)),
165 );
166 + if (integrity) {
167 + bootstrapChunks.push(
168 + scriptIntegirty,
169 + stringToChunk(escapeTextForBrowser(integrity)),
170 + );
171 + }
172 + bootstrapChunks.push(endAsyncScript);
173 }
174 }
175 return {