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 " { 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 {