diff --git a/packages/react-dom/src/__tests__/escapeScriptForBrowser-test.js b/packages/react-dom/src/__tests__/escapeScriptForBrowser-test.js
new file mode 100644
index 0000000000000..681daf78bea37
--- /dev/null
+++ b/packages/react-dom/src/__tests__/escapeScriptForBrowser-test.js
@@ -0,0 +1,107 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+let React;
+let ReactDOMFizzServer;
+let Stream;
+
+function getTestWritable() {
+ const writable = new Stream.PassThrough();
+ writable.setEncoding('utf8');
+ const output = {result: '', error: undefined};
+ writable.on('data', chunk => {
+ output.result += chunk;
+ });
+ writable.on('error', error => {
+ output.error = error;
+ });
+ const completed = new Promise(resolve => {
+ writable.on('finish', () => {
+ resolve();
+ });
+ writable.on('error', () => {
+ resolve();
+ });
+ });
+ return {writable, completed, output};
+}
+
+describe('escapeScriptForBrowser', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ React = require('react');
+ ReactDOMFizzServer = require('react-dom/server');
+ Stream = require('stream');
+ });
+
+ it('"<[Ss]cript" strings are replaced with unicode escaped lowercase s or S depending on case', () => {
+ const {writable, output} = getTestWritable();
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
, {
+ bootstrapScriptContent:
+ '"prescription pre',
+ );
+ });
+
+ it('"[Ss]cript" strings are replaced with encoded lowercase s or S depending on case', () => {
+ const {writable, output} = getTestWritable();
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(, {
+ bootstrapScriptContent:
+ '"prescription pre',
+ );
+ });
+
+ it('"[Ss]cript", "/[Ss]cript", "<[Ss]crip", "[Ss]crip" strings are not escaped', () => {
+ const {writable, output} = getTestWritable();
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(, {
+ bootstrapScriptContent:
+ '"Script script /Script /script ',
+ );
+ });
+
+ it('matches case insensitively', () => {
+ const {writable, output} = getTestWritable();
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(, {
+ bootstrapScriptContent: '"',
+ );
+ });
+
+ it('does not escape <, >, &, \\u2028, or \\u2029 characters', () => {
+ const {writable, output} = getTestWritable();
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(, {
+ bootstrapScriptContent: '"<, >, &, \u2028, or \u2029"',
+ });
+ pipe(writable);
+ jest.runAllTimers();
+ expect(output.result).toMatch(
+ '',
+ );
+ });
+});
diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
index a42cb3188722b..7b618fd601573 100644
--- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
@@ -52,6 +52,7 @@ import {validateProperties as validateUnknownProperties} from '../shared/ReactDO
import warnValidStyle from '../shared/warnValidStyle';
import escapeTextForBrowser from './escapeTextForBrowser';
+import escapeScriptForBrowser from './escapeScriptForBrowser';
import hyphenateStyleName from '../shared/hyphenateStyleName';
import hasOwnProperty from 'shared/hasOwnProperty';
import sanitizeURL from '../shared/sanitizeURL';
@@ -102,7 +103,7 @@ export function createResponseState(
if (bootstrapScriptContent !== undefined) {
bootstrapChunks.push(
inlineScriptWithNonce,
- stringToChunk(escapeTextForBrowser(bootstrapScriptContent)),
+ stringToChunk(escapeScriptForBrowser(bootstrapScriptContent)),
endInlineScript,
);
}
diff --git a/packages/react-dom/src/server/escapeScriptForBrowser.js b/packages/react-dom/src/server/escapeScriptForBrowser.js
new file mode 100644
index 0000000000000..fb79e190014f7
--- /dev/null
+++ b/packages/react-dom/src/server/escapeScriptForBrowser.js
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import {checkHtmlStringCoercion} from 'shared/CheckStringCoercion';
+
+const scriptRegex = /(<\/|<)(s)(cript)/gi;
+const scriptReplacer = (match, prefix, s, suffix) =>
+ `${prefix}${subsitutions[s]}${suffix}`;
+const subsitutions = {
+ s: '\\u0073',
+ S: '\\u0053',
+};
+
+/**
+ * Escapes javascript for embedding into HTML.
+ *
+ * @param {*} scriptText Text value to escape.
+ * @return {string} An escaped string.
+ */
+function escapeScriptForBrowser(scriptText) {
+ if (typeof scriptText === 'boolean' || typeof scriptText === 'number') {
+ // this shortcircuit helps perf for types that we know will never have
+ // special characters, especially given that this function is used often
+ // for numeric dom ids.
+ return '' + scriptText;
+ }
+ if (__DEV__) {
+ checkHtmlStringCoercion(scriptText);
+ }
+ return ('' + scriptText).replace(scriptRegex, scriptReplacer);
+}
+
+export default escapeScriptForBrowser;