Skip to content

Commit 11ea57a

Browse files
committed
Escape bootstrapScriptContent for javascript embedding into HTML
The previous escape was for Text into HTML and breaks script contents. The new escaping ensures that the script contents cannot prematurely close the host script tag by escaping script open and close string sequences using a unicode escape substitution.
1 parent ddb1ab1 commit 11ea57a

File tree

3 files changed

+146
-1
lines changed

3 files changed

+146
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails react-core
8+
*/
9+
10+
'use strict';
11+
12+
let React;
13+
let ReactDOMFizzServer;
14+
let Stream;
15+
16+
function getTestWritable() {
17+
const writable = new Stream.PassThrough();
18+
writable.setEncoding('utf8');
19+
const output = {result: '', error: undefined};
20+
writable.on('data', chunk => {
21+
output.result += chunk;
22+
});
23+
writable.on('error', error => {
24+
output.error = error;
25+
});
26+
const completed = new Promise(resolve => {
27+
writable.on('finish', () => {
28+
resolve();
29+
});
30+
writable.on('error', () => {
31+
resolve();
32+
});
33+
});
34+
return {writable, completed, output};
35+
}
36+
37+
describe('escapeScriptForBrowser', () => {
38+
beforeEach(() => {
39+
jest.resetModules();
40+
React = require('react');
41+
ReactDOMFizzServer = require('react-dom/server');
42+
Stream = require('stream');
43+
});
44+
45+
it('"<[Ss]cript" strings are replaced with unicode escaped lowercase s or S depending on case', () => {
46+
const {writable, output} = getTestWritable();
47+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<div />, {
48+
bootstrapScriptContent:
49+
'"prescription pre<scription preScription pre<Scription"',
50+
});
51+
pipe(writable);
52+
jest.runAllTimers();
53+
expect(output.result).toMatch(
54+
'<div></div><script>"prescription pre<\\u0073cription preScription pre<\\u0053cription"</script>',
55+
);
56+
});
57+
58+
it('"</[Ss]cript" strings are replaced with encoded lowercase s or S depending on case', () => {
59+
const {writable, output} = getTestWritable();
60+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<div />, {
61+
bootstrapScriptContent:
62+
'"prescription pre</scription preScription pre</Scription"',
63+
});
64+
pipe(writable);
65+
jest.runAllTimers();
66+
expect(output.result).toMatch(
67+
'<div></div><script>"prescription pre</\\u0073cription preScription pre</\\u0053cription"</script>',
68+
);
69+
});
70+
71+
it('"[Ss]cript", "/[Ss]cript", "<[Ss]crip", "</[Ss]crip" strings are not escaped', () => {
72+
const {writable, output} = getTestWritable();
73+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<div />, {
74+
bootstrapScriptContent:
75+
'"Script script /Script /script <Scrip <scrip </Scrip </scrip"',
76+
});
77+
pipe(writable);
78+
jest.runAllTimers();
79+
expect(output.result).toMatch(
80+
'<div></div><script>"Script script /Script /script <Scrip <scrip </Scrip </scrip"</script>',
81+
);
82+
});
83+
84+
it('matches case insensitively', () => {
85+
const {writable, output} = getTestWritable();
86+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<div />, {
87+
bootstrapScriptContent: '"<sCrIpT <ScripT </scrIPT </SCRIpT"',
88+
});
89+
pipe(writable);
90+
jest.runAllTimers();
91+
expect(output.result).toMatch(
92+
'<div></div><script>"<\\u0073CrIpT <\\u0053cripT </\\u0073crIPT </\\u0053CRIpT"</script>',
93+
);
94+
});
95+
96+
it('does not escape <, >, &, \\u2028, or \\u2029 characters', () => {
97+
const {writable, output} = getTestWritable();
98+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<div />, {
99+
bootstrapScriptContent: '"<, >, &, \u2028, or \u2029"',
100+
});
101+
pipe(writable);
102+
jest.runAllTimers();
103+
expect(output.result).toMatch(
104+
'<div></div><script>"<, >, &, \u2028, or \u2029"</script>',
105+
);
106+
});
107+
});

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

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

5454
import escapeTextForBrowser from './escapeTextForBrowser';
55+
import escapeScriptForBrowser from './escapeScriptForBrowser';
5556
import hyphenateStyleName from '../shared/hyphenateStyleName';
5657
import hasOwnProperty from 'shared/hasOwnProperty';
5758
import sanitizeURL from '../shared/sanitizeURL';
@@ -102,7 +103,7 @@ export function createResponseState(
102103
if (bootstrapScriptContent !== undefined) {
103104
bootstrapChunks.push(
104105
inlineScriptWithNonce,
105-
stringToChunk(escapeTextForBrowser(bootstrapScriptContent)),
106+
stringToChunk(escapeScriptForBrowser(bootstrapScriptContent)),
106107
endInlineScript,
107108
);
108109
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {checkHtmlStringCoercion} from 'shared/CheckStringCoercion';
9+
10+
const scriptRegex = /(<\/|<)(s)(cript)/gi;
11+
const scriptReplacer = (match, prefix, s, suffix) =>
12+
`${prefix}${subsitutions[s]}${suffix}`;
13+
const subsitutions = {
14+
s: '\\u0073',
15+
S: '\\u0053',
16+
};
17+
18+
/**
19+
* Escapes javascript for embedding into HTML.
20+
*
21+
* @param {*} scriptText Text value to escape.
22+
* @return {string} An escaped string.
23+
*/
24+
function escapeScriptForBrowser(scriptText) {
25+
if (typeof scriptText === 'boolean' || typeof scriptText === 'number') {
26+
// this shortcircuit helps perf for types that we know will never have
27+
// special characters, especially given that this function is used often
28+
// for numeric dom ids.
29+
return '' + scriptText;
30+
}
31+
if (__DEV__) {
32+
checkHtmlStringCoercion(scriptText);
33+
}
34+
return ('' + scriptText).replace(scriptRegex, scriptReplacer);
35+
}
36+
37+
export default escapeScriptForBrowser;

0 commit comments

Comments
 (0)