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 {