Skip to content

Commit 3443526

Browse files
committed
[Fizz] escape <script> textContent similar to bootstrapScript (#28871)
stacked on #28870 inline script children have been encoded as HTML for a while now but this can easily break script parsing so practically if you were rendering inline scripts you were using dangerouslySetInnerHTML. This is not great because now there is no escaping at all so you have to be even more careful. While care should always be taken when rendering untrusted script content driving users to use dangerous APIs is not the right approach and in this PR the escaping functionality used for bootstrapScripts and importMaps is being extended to any inline script. the approach is to escape 's' or 'S" with the appropriate unicode code point if it is inside a <script or </script sequence. This has the nice benefit of minimally escaping the text for readability while still preserving full js parsing capabilities. As articulated when we introduced this escaping for prior use cases this is only safe because we are escaping the entire script content. It would be unsafe if we were not escaping the entirety of the script because we would no longer be able to ensure there are no earlier or later <script sequences that put the parser in unexpected states. DiffTrain build for [561c023](561c023)
1 parent e7ed683 commit 3443526

7 files changed

+29
-32
lines changed

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
aead514db2808a2e82c128aa4db459939ab88b58
1+
561c023708bc0cb04613f89da821dc3c55245f01

compiled/facebook-www/ReactDOMServer-dev.classic.js

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ if (__DEV__) {
1919
var React = require("react");
2020
var ReactDOM = require("react-dom");
2121

22-
var ReactVersion = "19.0.0-www-classic-8765e454";
22+
var ReactVersion = "19.0.0-www-classic-477ccba7";
2323

2424
// This refers to a WWW module.
2525
var warningWWW = require("warning");
@@ -2454,8 +2454,8 @@ if (__DEV__) {
24542454
var scriptCrossOrigin = stringToPrecomputedChunk('" crossorigin="');
24552455
var endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
24562456
/**
2457-
* This escaping function is designed to work with bootstrapScriptContent and importMap only.
2458-
* because we know we are escaping the entire script. We can avoid for instance
2457+
* This escaping function is designed to work with with inline scripts where the entire
2458+
* contents are escaped. Because we know we are escaping the entire script we can avoid for instance
24592459
* escaping html comment string sequences that are valid javascript as well because
24602460
* if there are no sebsequent <script sequences the html parser will never enter
24612461
* script data double escaped state (see: https://www.w3.org/TR/html53/syntax.html#script-data-double-escaped-state)
@@ -2464,7 +2464,7 @@ if (__DEV__) {
24642464
* ensure that the script cannot be early terminated or never terminated state
24652465
*/
24662466

2467-
function escapeBootstrapAndImportMapScriptContent(scriptText) {
2467+
function escapeEntireInlineScriptContent(scriptText) {
24682468
{
24692469
checkHtmlStringCoercion(scriptText);
24702470
}
@@ -2523,7 +2523,7 @@ if (__DEV__) {
25232523
bootstrapChunks.push(
25242524
inlineScriptWithNonce,
25252525
stringToChunk(
2526-
escapeBootstrapAndImportMapScriptContent(bootstrapScriptContent)
2526+
escapeEntireInlineScriptContent(bootstrapScriptContent)
25272527
),
25282528
endInlineScript
25292529
);
@@ -2563,9 +2563,7 @@ if (__DEV__) {
25632563
var map = importMap;
25642564
importMapChunks.push(importMapScriptStart);
25652565
importMapChunks.push(
2566-
stringToChunk(
2567-
escapeBootstrapAndImportMapScriptContent(JSON.stringify(map))
2568-
)
2566+
stringToChunk(escapeEntireInlineScriptContent(JSON.stringify(map)))
25692567
);
25702568
importMapChunks.push(importMapScriptEnd);
25712569
}
@@ -5526,7 +5524,7 @@ if (__DEV__) {
55265524
pushInnerHTML(target, innerHTML, children);
55275525

55285526
if (typeof children === "string") {
5529-
target.push(stringToChunk(encodeHTMLTextNode(children)));
5527+
target.push(stringToChunk(escapeEntireInlineScriptContent(children)));
55305528
}
55315529

55325530
target.push(endChunkForTag("script"));

compiled/facebook-www/ReactDOMServer-dev.modern.js

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ if (__DEV__) {
1919
var React = require("react");
2020
var ReactDOM = require("react-dom");
2121

22-
var ReactVersion = "19.0.0-www-modern-e48dd4e1";
22+
var ReactVersion = "19.0.0-www-modern-d1bfd9cd";
2323

2424
// This refers to a WWW module.
2525
var warningWWW = require("warning");
@@ -2454,8 +2454,8 @@ if (__DEV__) {
24542454
var scriptCrossOrigin = stringToPrecomputedChunk('" crossorigin="');
24552455
var endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
24562456
/**
2457-
* This escaping function is designed to work with bootstrapScriptContent and importMap only.
2458-
* because we know we are escaping the entire script. We can avoid for instance
2457+
* This escaping function is designed to work with with inline scripts where the entire
2458+
* contents are escaped. Because we know we are escaping the entire script we can avoid for instance
24592459
* escaping html comment string sequences that are valid javascript as well because
24602460
* if there are no sebsequent <script sequences the html parser will never enter
24612461
* script data double escaped state (see: https://www.w3.org/TR/html53/syntax.html#script-data-double-escaped-state)
@@ -2464,7 +2464,7 @@ if (__DEV__) {
24642464
* ensure that the script cannot be early terminated or never terminated state
24652465
*/
24662466

2467-
function escapeBootstrapAndImportMapScriptContent(scriptText) {
2467+
function escapeEntireInlineScriptContent(scriptText) {
24682468
{
24692469
checkHtmlStringCoercion(scriptText);
24702470
}
@@ -2523,7 +2523,7 @@ if (__DEV__) {
25232523
bootstrapChunks.push(
25242524
inlineScriptWithNonce,
25252525
stringToChunk(
2526-
escapeBootstrapAndImportMapScriptContent(bootstrapScriptContent)
2526+
escapeEntireInlineScriptContent(bootstrapScriptContent)
25272527
),
25282528
endInlineScript
25292529
);
@@ -2563,9 +2563,7 @@ if (__DEV__) {
25632563
var map = importMap;
25642564
importMapChunks.push(importMapScriptStart);
25652565
importMapChunks.push(
2566-
stringToChunk(
2567-
escapeBootstrapAndImportMapScriptContent(JSON.stringify(map))
2568-
)
2566+
stringToChunk(escapeEntireInlineScriptContent(JSON.stringify(map)))
25692567
);
25702568
importMapChunks.push(importMapScriptEnd);
25712569
}
@@ -5526,7 +5524,7 @@ if (__DEV__) {
55265524
pushInnerHTML(target, innerHTML, children);
55275525

55285526
if (typeof children === "string") {
5529-
target.push(stringToChunk(encodeHTMLTextNode(children)));
5527+
target.push(stringToChunk(escapeEntireInlineScriptContent(children)));
55305528
}
55315529

55325530
target.push(endChunkForTag("script"));

compiled/facebook-www/ReactDOMServer-prod.classic.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,8 @@ function pushScriptImpl(target, props) {
782782
}
783783
target.push(">");
784784
pushInnerHTML(target, innerHTML, children);
785-
"string" === typeof children && target.push(escapeTextForBrowser(children));
785+
"string" === typeof children &&
786+
target.push(("" + children).replace(scriptRegex, scriptReplacer));
786787
target.push(endChunkForTag("script"));
787788
return null;
788789
}
@@ -5686,4 +5687,4 @@ exports.renderToString = function (children, options) {
56865687
'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server'
56875688
);
56885689
};
5689-
exports.version = "19.0.0-www-classic-a5b1d991";
5690+
exports.version = "19.0.0-www-classic-77d529ff";

compiled/facebook-www/ReactDOMServer-prod.modern.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,8 @@ function pushScriptImpl(target, props) {
782782
}
783783
target.push(">");
784784
pushInnerHTML(target, innerHTML, children);
785-
"string" === typeof children && target.push(escapeTextForBrowser(children));
785+
"string" === typeof children &&
786+
target.push(("" + children).replace(scriptRegex, scriptReplacer));
786787
target.push(endChunkForTag("script"));
787788
return null;
788789
}
@@ -5664,4 +5665,4 @@ exports.renderToString = function (children, options) {
56645665
'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server'
56655666
);
56665667
};
5667-
exports.version = "19.0.0-www-modern-5ad75306";
5668+
exports.version = "19.0.0-www-modern-322b4431";

compiled/facebook-www/ReactDOMServerStreaming-dev.modern.js

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2451,8 +2451,8 @@ if (__DEV__) {
24512451
var scriptCrossOrigin = stringToPrecomputedChunk('" crossorigin="');
24522452
var endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
24532453
/**
2454-
* This escaping function is designed to work with bootstrapScriptContent and importMap only.
2455-
* because we know we are escaping the entire script. We can avoid for instance
2454+
* This escaping function is designed to work with with inline scripts where the entire
2455+
* contents are escaped. Because we know we are escaping the entire script we can avoid for instance
24562456
* escaping html comment string sequences that are valid javascript as well because
24572457
* if there are no sebsequent <script sequences the html parser will never enter
24582458
* script data double escaped state (see: https://www.w3.org/TR/html53/syntax.html#script-data-double-escaped-state)
@@ -2461,7 +2461,7 @@ if (__DEV__) {
24612461
* ensure that the script cannot be early terminated or never terminated state
24622462
*/
24632463

2464-
function escapeBootstrapAndImportMapScriptContent(scriptText) {
2464+
function escapeEntireInlineScriptContent(scriptText) {
24652465
{
24662466
checkHtmlStringCoercion(scriptText);
24672467
}
@@ -2520,7 +2520,7 @@ if (__DEV__) {
25202520
bootstrapChunks.push(
25212521
inlineScriptWithNonce,
25222522
stringToChunk(
2523-
escapeBootstrapAndImportMapScriptContent(bootstrapScriptContent)
2523+
escapeEntireInlineScriptContent(bootstrapScriptContent)
25242524
),
25252525
endInlineScript
25262526
);
@@ -2560,9 +2560,7 @@ if (__DEV__) {
25602560
var map = importMap;
25612561
importMapChunks.push(importMapScriptStart);
25622562
importMapChunks.push(
2563-
stringToChunk(
2564-
escapeBootstrapAndImportMapScriptContent(JSON.stringify(map))
2565-
)
2563+
stringToChunk(escapeEntireInlineScriptContent(JSON.stringify(map)))
25662564
);
25672565
importMapChunks.push(importMapScriptEnd);
25682566
}
@@ -5523,7 +5521,7 @@ if (__DEV__) {
55235521
pushInnerHTML(target, innerHTML, children);
55245522

55255523
if (typeof children === "string") {
5526-
target.push(stringToChunk(encodeHTMLTextNode(children)));
5524+
target.push(stringToChunk(escapeEntireInlineScriptContent(children)));
55275525
}
55285526

55295527
target.push(endChunkForTag("script"));

compiled/facebook-www/ReactDOMServerStreaming-prod.modern.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -767,7 +767,8 @@ function pushScriptImpl(target, props) {
767767
}
768768
target.push(">");
769769
pushInnerHTML(target, innerHTML, children);
770-
"string" === typeof children && target.push(escapeTextForBrowser(children));
770+
"string" === typeof children &&
771+
target.push(("" + children).replace(scriptRegex, scriptReplacer));
771772
target.push(endChunkForTag("script"));
772773
return null;
773774
}

0 commit comments

Comments
 (0)