Skip to content

Commit 7dd5cc7

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 be5bc4d commit 7dd5cc7

File tree

4 files changed

+75
-146
lines changed

4 files changed

+75
-146
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

Lines changed: 58 additions & 0 deletions
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+
let 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+
let 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+
let 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
});

packages/react-dom/src/__tests__/escapeScriptForBrowser-test.js

Lines changed: 0 additions & 107 deletions
This file was deleted.

packages/react-dom/src/server/ReactDOMServerFormatConfig.js

Lines changed: 17 additions & 2 deletions
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
}

packages/react-dom/src/server/escapeScriptForBrowser.js

Lines changed: 0 additions & 37 deletions
This file was deleted.

0 commit comments

Comments
 (0)