Skip to content

Commit 52786bf

Browse files
committed
de-generalize to specific use case and refactor tests
the escaping of this function does is tailored to the specific use case of how bootstrapScriptContent is currently set up and having it be a module suggests it is meant for a more general than it has been considered for. Additionally the tests were redone to focus on practical implications for what is and is not escaped
1 parent 11483ce commit 52786bf

File tree

4 files changed

+75
-146
lines changed

4 files changed

+75
-146
lines changed

Diff for: packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

+58
Original file line numberDiff line numberDiff line change
@@ -2811,4 +2811,62 @@ describe('ReactDOMFizzServer', () => {
28112811
</ul>,
28122812
);
28132813
});
2814+
2815+
describe('bootstrapScriptContent escaping', () => {
2816+
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 () => {
2817+
window.__test_outlet = '';
2818+
const stringWithScriptsInIt =
2819+
'prescription pre<scription pre<Scription pre</scRipTion pre</ScripTion </script><script><!-- <script> -->';
2820+
await act(async () => {
2821+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<div />, {
2822+
bootstrapScriptContent:
2823+
'window.__test_outlet = "This should have been replaced";var x = "' +
2824+
stringWithScriptsInIt +
2825+
'";\nwindow.__test_outlet = x;',
2826+
});
2827+
pipe(writable);
2828+
});
2829+
expect(window.__test_outlet).toMatch(stringWithScriptsInIt);
2830+
});
2831+
2832+
it('does not escape \\u2028, or \\u2029 characters', async () => {
2833+
// these characters are ignored in engines support https://github.com/tc39/proposal-json-superset
2834+
// in this test with JSDOM the characters are silently dropped and thus don't need to be encoded.
2835+
// if you send these characters to an older browser they could fail so it is a good idea to
2836+
// sanitize JSON input of these characters
2837+
window.__test_outlet = '';
2838+
const el = document.createElement('p');
2839+
el.textContent = '{"one":1,\u2028\u2029"two":2}';
2840+
const stringWithLSAndPSCharacters = el.textContent;
2841+
await act(async () => {
2842+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<div />, {
2843+
bootstrapScriptContent:
2844+
'let x = ' +
2845+
stringWithLSAndPSCharacters +
2846+
'; window.__test_outlet = x;',
2847+
});
2848+
pipe(writable);
2849+
});
2850+
const outletString = JSON.stringify(window.__test_outlet);
2851+
expect(outletString).toBe(
2852+
stringWithLSAndPSCharacters.replace(/[\u2028\u2029]/g, ''),
2853+
);
2854+
});
2855+
2856+
it('does not escape <, >, or & characters', async () => {
2857+
// these characters valid javascript and may be necessary in scripts and won't be interpretted properly
2858+
// escaped outside of a string context within javascript
2859+
window.__test_outlet = null;
2860+
// this boolean expression will be cast to a number due to the bitwise &. we will look for a truthy value (1) below
2861+
const booleanLogicString = '1 < 2 & 3 > 1';
2862+
await act(async () => {
2863+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<div />, {
2864+
bootstrapScriptContent:
2865+
'let x = ' + booleanLogicString + '; window.__test_outlet = x;',
2866+
});
2867+
pipe(writable);
2868+
});
2869+
expect(window.__test_outlet).toBe(1);
2870+
});
2871+
});
28142872
});

Diff for: packages/react-dom/src/__tests__/escapeScriptForBrowser-test.js

-107
This file was deleted.

Diff for: packages/react-dom/src/server/ReactDOMServerFormatConfig.js

+17-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ import {validateProperties as validateUnknownProperties} from '../shared/ReactDO
5252
import warnValidStyle from '../shared/warnValidStyle';
5353

5454
import escapeTextForBrowser from './escapeTextForBrowser';
55-
import escapeScriptForBrowser from './escapeScriptForBrowser';
5655
import hyphenateStyleName from '../shared/hyphenateStyleName';
5756
import hasOwnProperty from 'shared/hasOwnProperty';
5857
import sanitizeURL from '../shared/sanitizeURL';
@@ -84,6 +83,22 @@ const startScriptSrc = stringToPrecomputedChunk('<script src="');
8483
const startModuleSrc = stringToPrecomputedChunk('<script type="module" src="');
8584
const endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
8685

86+
const scriptRegex = /(<\/|<)(s)(cript)/gi;
87+
const substitutions = {
88+
s: '\\u0073',
89+
S: '\\u0053',
90+
};
91+
92+
function escapeBootstrapScriptContent(scriptText) {
93+
if (__DEV__) {
94+
checkHtmlStringCoercion(scriptText);
95+
}
96+
return ('' + scriptText).replace(
97+
scriptRegex,
98+
(match, prefix, s, suffix) => `${prefix}${substitutions[s]}${suffix}`,
99+
);
100+
}
101+
87102
// Allows us to keep track of what we've already written so we can refer back to it.
88103
export function createResponseState(
89104
identifierPrefix: string | void,
@@ -103,7 +118,7 @@ export function createResponseState(
103118
if (bootstrapScriptContent !== undefined) {
104119
bootstrapChunks.push(
105120
inlineScriptWithNonce,
106-
stringToChunk(escapeScriptForBrowser(bootstrapScriptContent)),
121+
stringToChunk(escapeBootstrapScriptContent(bootstrapScriptContent)),
107122
endInlineScript,
108123
);
109124
}

Diff for: packages/react-dom/src/server/escapeScriptForBrowser.js

-37
This file was deleted.

0 commit comments

Comments
 (0)