From 1e5245df89e45b788acf3af3769f0d816b18458d Mon Sep 17 00:00:00 2001
From: Josh Story
Date: Wed, 17 Aug 2022 08:31:05 +0100
Subject: [PATCH] support subresource integrity for bootstrapScripts and
bootstrapModules (#25104)
---
.../src/__tests__/ReactDOMFizzServer-test.js | 55 +++++++++++++++++++
.../src/server/ReactDOMFizzServerBrowser.js | 5 +-
.../src/server/ReactDOMFizzServerNode.js | 5 +-
.../src/server/ReactDOMFizzStaticBrowser.js | 5 +-
.../src/server/ReactDOMFizzStaticNode.js | 6 +-
.../src/server/ReactDOMServerFormatConfig.js | 39 +++++++++++--
6 files changed, 101 insertions(+), 14 deletions(-)
diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
index 10d3237bf38d0..3812baffc7486 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
@@ -3390,6 +3390,61 @@ describe('ReactDOMFizzServer', () => {
});
});
+ it('accepts an integrity property for bootstrapScripts and bootstrapModules', async () => {
+ await actIntoEmptyDocument(() => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
+
+
+
+ hello world
+
+ ,
+ {
+ bootstrapScripts: [
+ 'foo',
+ {
+ src: 'bar',
+ },
+ {
+ src: 'baz',
+ integrity: 'qux',
+ },
+ ],
+ bootstrapModules: [
+ 'quux',
+ {
+ src: 'corge',
+ },
+ {
+ src: 'grault',
+ integrity: 'garply',
+ },
+ ],
+ },
+ );
+ pipe(writable);
+ });
+
+ expect(getVisibleChildren(document)).toEqual(
+
+
+ hello world
+
+ ,
+ );
+ expect(
+ Array.from(document.getElementsByTagName('script')).map(n => n.outerHTML),
+ ).toEqual([
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ ]);
+ });
+
describe('bootstrapScriptContent escaping', () => {
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 () => {
window.__test_outlet = '';
diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
index acfe13e4ebbcc..2f626c922aa1a 100644
--- a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
+++ b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
@@ -8,6 +8,7 @@
*/
import type {ReactNodeList} from 'shared/ReactTypes';
+import type {BootstrapScriptDescriptor} from './ReactDOMServerFormatConfig';
import ReactVersion from 'shared/ReactVersion';
@@ -28,8 +29,8 @@ type Options = {|
namespaceURI?: string,
nonce?: string,
bootstrapScriptContent?: string,
- bootstrapScripts?: Array,
- bootstrapModules?: Array,
+ bootstrapScripts?: Array,
+ bootstrapModules?: Array,
progressiveChunkSize?: number,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
diff --git a/packages/react-dom/src/server/ReactDOMFizzServerNode.js b/packages/react-dom/src/server/ReactDOMFizzServerNode.js
index 4728a48a0552f..6790c44bccaca 100644
--- a/packages/react-dom/src/server/ReactDOMFizzServerNode.js
+++ b/packages/react-dom/src/server/ReactDOMFizzServerNode.js
@@ -9,6 +9,7 @@
import type {ReactNodeList} from 'shared/ReactTypes';
import type {Writable} from 'stream';
+import type {BootstrapScriptDescriptor} from './ReactDOMServerFormatConfig';
import ReactVersion from 'shared/ReactVersion';
@@ -38,8 +39,8 @@ type Options = {|
namespaceURI?: string,
nonce?: string,
bootstrapScriptContent?: string,
- bootstrapScripts?: Array,
- bootstrapModules?: Array,
+ bootstrapScripts?: Array,
+ bootstrapModules?: Array,
progressiveChunkSize?: number,
onShellReady?: () => void,
onShellError?: (error: mixed) => void,
diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
index 989d2de31ba0d..38e4c948f6b9b 100644
--- a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
+++ b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
@@ -8,6 +8,7 @@
*/
import type {ReactNodeList} from 'shared/ReactTypes';
+import type {BootstrapScriptDescriptor} from './ReactDOMServerFormatConfig';
import ReactVersion from 'shared/ReactVersion';
@@ -27,8 +28,8 @@ type Options = {|
identifierPrefix?: string,
namespaceURI?: string,
bootstrapScriptContent?: string,
- bootstrapScripts?: Array,
- bootstrapModules?: Array,
+ bootstrapScripts?: Array,
+ bootstrapModules?: Array,
progressiveChunkSize?: number,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js
index e799c21a8b78c..b5f0022d12956 100644
--- a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js
+++ b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js
@@ -8,6 +8,8 @@
*/
import type {ReactNodeList} from 'shared/ReactTypes';
+import type {BootstrapScriptDescriptor} from './ReactDOMServerFormatConfig';
+
import {Writable, Readable} from 'stream';
import ReactVersion from 'shared/ReactVersion';
@@ -28,8 +30,8 @@ type Options = {|
identifierPrefix?: string,
namespaceURI?: string,
bootstrapScriptContent?: string,
- bootstrapScripts?: Array,
- bootstrapModules?: Array,
+ bootstrapScripts?: Array,
+ bootstrapModules?: Array,
progressiveChunkSize?: number,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
index 1cf876940d3fb..308e35a20b3ae 100644
--- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
@@ -82,6 +82,7 @@ const endInlineScript = stringToPrecomputedChunk('');
const startScriptSrc = stringToPrecomputedChunk('');
/**
@@ -104,13 +105,17 @@ const scriptRegex = /(<\/|<)(s)(cript)/gi;
const scriptReplacer = (match, prefix, s, suffix) =>
`${prefix}${s === 's' ? '\\u0073' : '\\u0053'}${suffix}`;
+export type BootstrapScriptDescriptor = {
+ src: string,
+ integrity?: string,
+};
// Allows us to keep track of what we've already written so we can refer back to it.
export function createResponseState(
identifierPrefix: string | void,
nonce: string | void,
bootstrapScriptContent: string | void,
- bootstrapScripts: Array | void,
- bootstrapModules: Array | void,
+ bootstrapScripts: $ReadOnlyArray | void,
+ bootstrapModules: $ReadOnlyArray | void,
): ResponseState {
const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;
const inlineScriptWithNonce =
@@ -129,20 +134,42 @@ export function createResponseState(
}
if (bootstrapScripts !== undefined) {
for (let i = 0; i < bootstrapScripts.length; i++) {
+ const scriptConfig = bootstrapScripts[i];
+ const src =
+ typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src;
+ const integrity =
+ typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;
bootstrapChunks.push(
startScriptSrc,
- stringToChunk(escapeTextForBrowser(bootstrapScripts[i])),
- endAsyncScript,
+ stringToChunk(escapeTextForBrowser(src)),
);
+ if (integrity) {
+ bootstrapChunks.push(
+ scriptIntegirty,
+ stringToChunk(escapeTextForBrowser(integrity)),
+ );
+ }
+ bootstrapChunks.push(endAsyncScript);
}
}
if (bootstrapModules !== undefined) {
for (let i = 0; i < bootstrapModules.length; i++) {
+ const scriptConfig = bootstrapModules[i];
+ const src =
+ typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src;
+ const integrity =
+ typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;
bootstrapChunks.push(
startModuleSrc,
- stringToChunk(escapeTextForBrowser(bootstrapModules[i])),
- endAsyncScript,
+ stringToChunk(escapeTextForBrowser(src)),
);
+ if (integrity) {
+ bootstrapChunks.push(
+ scriptIntegirty,
+ stringToChunk(escapeTextForBrowser(integrity)),
+ );
+ }
+ bootstrapChunks.push(endAsyncScript);
}
}
return {