Skip to content

Commit

Permalink
support subresource integrity for bootstrapScripts and bootstrapModules
Browse files Browse the repository at this point in the history
  • Loading branch information
gnoff committed Aug 16, 2022
1 parent 6ef466c commit 6aaf498
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 14 deletions.
55 changes: 55 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3390,6 +3390,61 @@ describe('ReactDOMFizzServer', () => {
});
});

it('accepts an integrity property for bootstrapScripts and bootstrapModules', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<div>hello world</div>
</body>
</html>,
{
bootstrapScripts: [
'foo',
{
src: 'bar',
},
{
src: 'baz',
integrity: 'qux',
},
],
bootstrapModules: [
'quux',
{
src: 'corge',
},
{
src: 'grault',
integrity: 'garply',
},
],
},
);
pipe(writable);
});

expect(getVisibleChildren(document)).toEqual(
<html>
<head />
<body>
<div>hello world</div>
</body>
</html>,
);
expect(
Array.from(document.getElementsByTagName('script')).map(n => n.outerHTML),
).toEqual([
'<script src="foo" async=""></script>',
'<script src="bar" async=""></script>',
'<script src="baz" integrity="qux" async=""></script>',
'<script type="module" src="quux" async=""></script>',
'<script type="module" src="corge" async=""></script>',
'<script type="module" src="grault" integrity="garply" async=""></script>',
]);
});

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 = '';
Expand Down
5 changes: 3 additions & 2 deletions packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import type {ReactNodeList} from 'shared/ReactTypes';
import type {BootstrapScriptDescriptor} from './ReactDOMServerFormatConfig';

import ReactVersion from 'shared/ReactVersion';

Expand All @@ -28,8 +29,8 @@ type Options = {|
namespaceURI?: string,
nonce?: string,
bootstrapScriptContent?: string,
bootstrapScripts?: Array<string>,
bootstrapModules?: Array<string>,
bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
progressiveChunkSize?: number,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
Expand Down
5 changes: 3 additions & 2 deletions packages/react-dom/src/server/ReactDOMFizzServerNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -38,8 +39,8 @@ type Options = {|
namespaceURI?: string,
nonce?: string,
bootstrapScriptContent?: string,
bootstrapScripts?: Array<string>,
bootstrapModules?: Array<string>,
bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
progressiveChunkSize?: number,
onShellReady?: () => void,
onShellError?: (error: mixed) => void,
Expand Down
5 changes: 3 additions & 2 deletions packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import type {ReactNodeList} from 'shared/ReactTypes';
import type {BootstrapScriptDescriptor} from './ReactDOMServerFormatConfig';

import ReactVersion from 'shared/ReactVersion';

Expand All @@ -27,8 +28,8 @@ type Options = {|
identifierPrefix?: string,
namespaceURI?: string,
bootstrapScriptContent?: string,
bootstrapScripts?: Array<string>,
bootstrapModules?: Array<string>,
bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
progressiveChunkSize?: number,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
Expand Down
6 changes: 4 additions & 2 deletions packages/react-dom/src/server/ReactDOMFizzStaticNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -28,8 +30,8 @@ type Options = {|
identifierPrefix?: string,
namespaceURI?: string,
bootstrapScriptContent?: string,
bootstrapScripts?: Array<string>,
bootstrapModules?: Array<string>,
bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
progressiveChunkSize?: number,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
Expand Down
39 changes: 33 additions & 6 deletions packages/react-dom/src/server/ReactDOMServerFormatConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const endInlineScript = stringToPrecomputedChunk('</script>');

const startScriptSrc = stringToPrecomputedChunk('<script src="');
const startModuleSrc = stringToPrecomputedChunk('<script type="module" src="');
const scriptIntegirty = stringToPrecomputedChunk('" integrity="');
const endAsyncScript = stringToPrecomputedChunk('" async=""></script>');

/**
Expand All @@ -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<string> | void,
bootstrapModules: Array<string> | void,
bootstrapScripts: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,
bootstrapModules: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,
): ResponseState {
const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;
const inlineScriptWithNonce =
Expand All @@ -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 {
Expand Down

0 comments on commit 6aaf498

Please sign in to comment.