Skip to content

Commit

Permalink
[Fizz] escape <script> textContent similar to bootstrapScript (#28871)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
gnoff committed Apr 19, 2024
1 parent e7ed683 commit 3443526
Show file tree
Hide file tree
Showing 7 changed files with 29 additions and 32 deletions.
2 changes: 1 addition & 1 deletion compiled/facebook-www/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
aead514db2808a2e82c128aa4db459939ab88b58
561c023708bc0cb04613f89da821dc3c55245f01
16 changes: 7 additions & 9 deletions compiled/facebook-www/ReactDOMServer-dev.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ if (__DEV__) {
var React = require("react");
var ReactDOM = require("react-dom");

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

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

function escapeBootstrapAndImportMapScriptContent(scriptText) {
function escapeEntireInlineScriptContent(scriptText) {
{
checkHtmlStringCoercion(scriptText);
}
Expand Down Expand Up @@ -2523,7 +2523,7 @@ if (__DEV__) {
bootstrapChunks.push(
inlineScriptWithNonce,
stringToChunk(
escapeBootstrapAndImportMapScriptContent(bootstrapScriptContent)
escapeEntireInlineScriptContent(bootstrapScriptContent)
),
endInlineScript
);
Expand Down Expand Up @@ -2563,9 +2563,7 @@ if (__DEV__) {
var map = importMap;
importMapChunks.push(importMapScriptStart);
importMapChunks.push(
stringToChunk(
escapeBootstrapAndImportMapScriptContent(JSON.stringify(map))
)
stringToChunk(escapeEntireInlineScriptContent(JSON.stringify(map)))
);
importMapChunks.push(importMapScriptEnd);
}
Expand Down Expand Up @@ -5526,7 +5524,7 @@ if (__DEV__) {
pushInnerHTML(target, innerHTML, children);

if (typeof children === "string") {
target.push(stringToChunk(encodeHTMLTextNode(children)));
target.push(stringToChunk(escapeEntireInlineScriptContent(children)));
}

target.push(endChunkForTag("script"));
Expand Down
16 changes: 7 additions & 9 deletions compiled/facebook-www/ReactDOMServer-dev.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ if (__DEV__) {
var React = require("react");
var ReactDOM = require("react-dom");

var ReactVersion = "19.0.0-www-modern-e48dd4e1";
var ReactVersion = "19.0.0-www-modern-d1bfd9cd";

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

function escapeBootstrapAndImportMapScriptContent(scriptText) {
function escapeEntireInlineScriptContent(scriptText) {
{
checkHtmlStringCoercion(scriptText);
}
Expand Down Expand Up @@ -2523,7 +2523,7 @@ if (__DEV__) {
bootstrapChunks.push(
inlineScriptWithNonce,
stringToChunk(
escapeBootstrapAndImportMapScriptContent(bootstrapScriptContent)
escapeEntireInlineScriptContent(bootstrapScriptContent)
),
endInlineScript
);
Expand Down Expand Up @@ -2563,9 +2563,7 @@ if (__DEV__) {
var map = importMap;
importMapChunks.push(importMapScriptStart);
importMapChunks.push(
stringToChunk(
escapeBootstrapAndImportMapScriptContent(JSON.stringify(map))
)
stringToChunk(escapeEntireInlineScriptContent(JSON.stringify(map)))
);
importMapChunks.push(importMapScriptEnd);
}
Expand Down Expand Up @@ -5526,7 +5524,7 @@ if (__DEV__) {
pushInnerHTML(target, innerHTML, children);

if (typeof children === "string") {
target.push(stringToChunk(encodeHTMLTextNode(children)));
target.push(stringToChunk(escapeEntireInlineScriptContent(children)));
}

target.push(endChunkForTag("script"));
Expand Down
5 changes: 3 additions & 2 deletions compiled/facebook-www/ReactDOMServer-prod.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -782,7 +782,8 @@ function pushScriptImpl(target, props) {
}
target.push(">");
pushInnerHTML(target, innerHTML, children);
"string" === typeof children && target.push(escapeTextForBrowser(children));
"string" === typeof children &&
target.push(("" + children).replace(scriptRegex, scriptReplacer));
target.push(endChunkForTag("script"));
return null;
}
Expand Down Expand Up @@ -5686,4 +5687,4 @@ exports.renderToString = function (children, options) {
'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'
);
};
exports.version = "19.0.0-www-classic-a5b1d991";
exports.version = "19.0.0-www-classic-77d529ff";
5 changes: 3 additions & 2 deletions compiled/facebook-www/ReactDOMServer-prod.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -782,7 +782,8 @@ function pushScriptImpl(target, props) {
}
target.push(">");
pushInnerHTML(target, innerHTML, children);
"string" === typeof children && target.push(escapeTextForBrowser(children));
"string" === typeof children &&
target.push(("" + children).replace(scriptRegex, scriptReplacer));
target.push(endChunkForTag("script"));
return null;
}
Expand Down Expand Up @@ -5664,4 +5665,4 @@ exports.renderToString = function (children, options) {
'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'
);
};
exports.version = "19.0.0-www-modern-5ad75306";
exports.version = "19.0.0-www-modern-322b4431";
14 changes: 6 additions & 8 deletions compiled/facebook-www/ReactDOMServerStreaming-dev.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -2451,8 +2451,8 @@ if (__DEV__) {
var scriptCrossOrigin = stringToPrecomputedChunk('" crossorigin="');
var endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
/**
* This escaping function is designed to work with bootstrapScriptContent and importMap only.
* because we know we are escaping the entire script. We can avoid for instance
* This escaping function is designed to work with with inline scripts where the entire
* contents are escaped. Because we know we are escaping the entire script we can avoid for instance
* escaping html comment string sequences that are valid javascript as well because
* if there are no sebsequent <script sequences the html parser will never enter
* script data double escaped state (see: https://www.w3.org/TR/html53/syntax.html#script-data-double-escaped-state)
Expand All @@ -2461,7 +2461,7 @@ if (__DEV__) {
* ensure that the script cannot be early terminated or never terminated state
*/

function escapeBootstrapAndImportMapScriptContent(scriptText) {
function escapeEntireInlineScriptContent(scriptText) {
{
checkHtmlStringCoercion(scriptText);
}
Expand Down Expand Up @@ -2520,7 +2520,7 @@ if (__DEV__) {
bootstrapChunks.push(
inlineScriptWithNonce,
stringToChunk(
escapeBootstrapAndImportMapScriptContent(bootstrapScriptContent)
escapeEntireInlineScriptContent(bootstrapScriptContent)
),
endInlineScript
);
Expand Down Expand Up @@ -2560,9 +2560,7 @@ if (__DEV__) {
var map = importMap;
importMapChunks.push(importMapScriptStart);
importMapChunks.push(
stringToChunk(
escapeBootstrapAndImportMapScriptContent(JSON.stringify(map))
)
stringToChunk(escapeEntireInlineScriptContent(JSON.stringify(map)))
);
importMapChunks.push(importMapScriptEnd);
}
Expand Down Expand Up @@ -5523,7 +5521,7 @@ if (__DEV__) {
pushInnerHTML(target, innerHTML, children);

if (typeof children === "string") {
target.push(stringToChunk(encodeHTMLTextNode(children)));
target.push(stringToChunk(escapeEntireInlineScriptContent(children)));
}

target.push(endChunkForTag("script"));
Expand Down
3 changes: 2 additions & 1 deletion compiled/facebook-www/ReactDOMServerStreaming-prod.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,8 @@ function pushScriptImpl(target, props) {
}
target.push(">");
pushInnerHTML(target, innerHTML, children);
"string" === typeof children && target.push(escapeTextForBrowser(children));
"string" === typeof children &&
target.push(("" + children).replace(scriptRegex, scriptReplacer));
target.push(endChunkForTag("script"));
return null;
}
Expand Down

0 comments on commit 3443526

Please sign in to comment.