Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Escape bootstrapScriptContent for javascript embedding into HTML #24385

Merged
merged 5 commits into from Apr 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
61 changes: 61 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Expand Up @@ -2811,4 +2811,65 @@ describe('ReactDOMFizzServer', () => {
</ul>,
);
});

describe('bootstrapScriptContent escaping', () => {
// @gate experimental
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 = '';
const stringWithScriptsInIt =
'prescription pre<scription pre<Scription pre</scRipTion pre</ScripTion </script><script><!-- <script> -->';
await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<div />, {
bootstrapScriptContent:
'window.__test_outlet = "This should have been replaced";var x = "' +
stringWithScriptsInIt +
'";\nwindow.__test_outlet = x;',
});
pipe(writable);
});
expect(window.__test_outlet).toMatch(stringWithScriptsInIt);
});

// @gate experimental
it('does not escape \\u2028, or \\u2029 characters', async () => {
// these characters are ignored in engines support https://github.com/tc39/proposal-json-superset
// in this test with JSDOM the characters are silently dropped and thus don't need to be encoded.
// if you send these characters to an older browser they could fail so it is a good idea to
// sanitize JSON input of these characters
window.__test_outlet = '';
const el = document.createElement('p');
el.textContent = '{"one":1,\u2028\u2029"two":2}';
const stringWithLSAndPSCharacters = el.textContent;
await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<div />, {
bootstrapScriptContent:
'let x = ' +
stringWithLSAndPSCharacters +
'; window.__test_outlet = x;',
});
pipe(writable);
});
const outletString = JSON.stringify(window.__test_outlet);
expect(outletString).toBe(
stringWithLSAndPSCharacters.replace(/[\u2028\u2029]/g, ''),
);
});

// @gate experimental
it('does not escape <, >, or & characters', async () => {
// these characters valid javascript and may be necessary in scripts and won't be interpretted properly
// escaped outside of a string context within javascript
window.__test_outlet = null;
// this boolean expression will be cast to a number due to the bitwise &. we will look for a truthy value (1) below
const booleanLogicString = '1 < 2 & 3 > 1';
await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<div />, {
bootstrapScriptContent:
'let x = ' + booleanLogicString + '; window.__test_outlet = x;',
});
pipe(writable);
});
expect(window.__test_outlet).toBe(1);
});
});
});
13 changes: 12 additions & 1 deletion packages/react-dom/src/server/ReactDOMServerFormatConfig.js
Expand Up @@ -83,6 +83,17 @@ const startScriptSrc = stringToPrecomputedChunk('<script src="');
const startModuleSrc = stringToPrecomputedChunk('<script type="module" src="');
const endAsyncScript = stringToPrecomputedChunk('" async=""></script>');

const scriptRegex = /(<\/|<)(s)(cript)/gi;
const scriptReplacer = (match, prefix, s, suffix) =>
`${prefix}${s === 's' ? '\\u0073' : '\\u0053'}${suffix}`;

function escapeBootstrapScriptContent(scriptText) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a leave a comment with the rationale and clarifying that this only works if the whole content is encoded and if it's an actual JS script tag, and doesn't work for attributes or CSS.

if (__DEV__) {
checkHtmlStringCoercion(scriptText);
}
return ('' + scriptText).replace(scriptRegex, scriptReplacer);
}

// Allows us to keep track of what we've already written so we can refer back to it.
export function createResponseState(
identifierPrefix: string | void,
Expand All @@ -102,7 +113,7 @@ export function createResponseState(
if (bootstrapScriptContent !== undefined) {
bootstrapChunks.push(
inlineScriptWithNonce,
stringToChunk(escapeTextForBrowser(bootstrapScriptContent)),
stringToChunk(escapeBootstrapScriptContent(bootstrapScriptContent)),
endInlineScript,
);
}
Expand Down