Skip to content

Commit e7ed683

Browse files
committed
[Fizz] escape <style> textContent as css (#28870)
style text content has historically been escaped as HTML which is non-sensical and often leads users to using dangerouslySetInnerHTML as a matter of course. While rendering untrusted style rules is a security risk React doesn't really provide any special protection here and forcing users to use a completely unescaped API is if anything worse. So this PR updates the style escaping rules for Fizz to only escape the text content to ensure the tag scope cannot be closed early. This is accomplished by encoding "s" and "S" as hexadecimal unicode representation "\73 " and "\53 " respectively when found within a sequence like </style>. We have to be careful to support casing here just like with the script closing tag regex for bootstrap scripts. DiffTrain build for [aead514](aead514)
1 parent f29aad9 commit e7ed683

File tree

7 files changed

+95
-17
lines changed

7 files changed

+95
-17
lines changed

compiled/facebook-www/REVISION

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

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

Lines changed: 23 additions & 3 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-9835bfc2";
22+
var ReactVersion = "19.0.0-www-classic-8765e454";
2323

2424
// This refers to a WWW module.
2525
var warningWWW = require("warning");
@@ -4924,6 +4924,26 @@ if (__DEV__) {
49244924
target.push(textSeparator);
49254925
}
49264926
}
4927+
/**
4928+
* This escaping function is designed to work with style tag textContent only.
4929+
*
4930+
* While untrusted style content should be made safe before using this api it will
4931+
* ensure that the style cannot be early terminated or never terminated state
4932+
*/
4933+
4934+
function escapeStyleTextContent(styleText) {
4935+
{
4936+
checkHtmlStringCoercion(styleText);
4937+
}
4938+
4939+
return ("" + styleText).replace(styleRegex, styleReplacer);
4940+
}
4941+
4942+
var styleRegex = /(<\/|<)(s)(tyle)/gi;
4943+
4944+
var styleReplacer = function (match, prefix, s, suffix) {
4945+
return "" + prefix + (s === "s" ? "\\73 " : "\\53 ") + suffix;
4946+
};
49274947

49284948
function pushStyleImpl(target, props) {
49294949
target.push(startChunkForTag("style"));
@@ -4968,7 +4988,7 @@ if (__DEV__) {
49684988
child !== undefined
49694989
) {
49704990
// eslint-disable-next-line react-internal/safe-string-coercion
4971-
target.push(stringToChunk(escapeTextForBrowser("" + child)));
4991+
target.push(stringToChunk(escapeStyleTextContent(child)));
49724992
}
49734993

49744994
pushInnerHTML(target, innerHTML, children);
@@ -5013,7 +5033,7 @@ if (__DEV__) {
50135033
child !== undefined
50145034
) {
50155035
// eslint-disable-next-line react-internal/safe-string-coercion
5016-
target.push(stringToChunk(escapeTextForBrowser("" + child)));
5036+
target.push(stringToChunk(escapeStyleTextContent(child)));
50175037
}
50185038

50195039
pushInnerHTML(target, innerHTML, children);

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

Lines changed: 23 additions & 3 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-72ca4dea";
22+
var ReactVersion = "19.0.0-www-modern-e48dd4e1";
2323

2424
// This refers to a WWW module.
2525
var warningWWW = require("warning");
@@ -4924,6 +4924,26 @@ if (__DEV__) {
49244924
target.push(textSeparator);
49254925
}
49264926
}
4927+
/**
4928+
* This escaping function is designed to work with style tag textContent only.
4929+
*
4930+
* While untrusted style content should be made safe before using this api it will
4931+
* ensure that the style cannot be early terminated or never terminated state
4932+
*/
4933+
4934+
function escapeStyleTextContent(styleText) {
4935+
{
4936+
checkHtmlStringCoercion(styleText);
4937+
}
4938+
4939+
return ("" + styleText).replace(styleRegex, styleReplacer);
4940+
}
4941+
4942+
var styleRegex = /(<\/|<)(s)(tyle)/gi;
4943+
4944+
var styleReplacer = function (match, prefix, s, suffix) {
4945+
return "" + prefix + (s === "s" ? "\\73 " : "\\53 ") + suffix;
4946+
};
49274947

49284948
function pushStyleImpl(target, props) {
49294949
target.push(startChunkForTag("style"));
@@ -4968,7 +4988,7 @@ if (__DEV__) {
49684988
child !== undefined
49694989
) {
49704990
// eslint-disable-next-line react-internal/safe-string-coercion
4971-
target.push(stringToChunk(escapeTextForBrowser("" + child)));
4991+
target.push(stringToChunk(escapeStyleTextContent(child)));
49724992
}
49734993

49744994
pushInnerHTML(target, innerHTML, children);
@@ -5013,7 +5033,7 @@ if (__DEV__) {
50135033
child !== undefined
50145034
) {
50155035
// eslint-disable-next-line react-internal/safe-string-coercion
5016-
target.push(stringToChunk(escapeTextForBrowser("" + child)));
5036+
target.push(stringToChunk(escapeStyleTextContent(child)));
50175037
}
50185038

50195039
pushInnerHTML(target, innerHTML, children);

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,10 @@ function pushLinkImpl(target, props) {
704704
target.push("/>");
705705
return null;
706706
}
707+
var styleRegex = /(<\/|<)(s)(tyle)/gi;
708+
function styleReplacer(match, prefix, s, suffix) {
709+
return "" + prefix + ("s" === s ? "\\73 " : "\\53 ") + suffix;
710+
}
707711
function pushSelfClosing(target, props, tag) {
708712
target.push(startChunkForTag(tag));
709713
for (var propKey in props)
@@ -1435,7 +1439,7 @@ function pushStartInstance(
14351439
"symbol" !== typeof child &&
14361440
null !== child &&
14371441
void 0 !== child &&
1438-
target$jscomp$0.push(escapeTextForBrowser("" + child));
1442+
target$jscomp$0.push(("" + child).replace(styleRegex, styleReplacer));
14391443
pushInnerHTML(target$jscomp$0, innerHTML$jscomp$4, children$jscomp$5);
14401444
target$jscomp$0.push(endChunkForTag("style"));
14411445
var JSCompiler_inline_result$jscomp$5 = null;
@@ -1484,7 +1488,9 @@ function pushStartInstance(
14841488
"symbol" !== typeof child$jscomp$0 &&
14851489
null !== child$jscomp$0 &&
14861490
void 0 !== child$jscomp$0 &&
1487-
target.push(escapeTextForBrowser("" + child$jscomp$0));
1491+
target.push(
1492+
("" + child$jscomp$0).replace(styleRegex, styleReplacer)
1493+
);
14881494
pushInnerHTML(target, innerHTML$jscomp$5, children$jscomp$6);
14891495
}
14901496
styleQueue$jscomp$0 &&
@@ -5680,4 +5686,4 @@ exports.renderToString = function (children, options) {
56805686
'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'
56815687
);
56825688
};
5683-
exports.version = "19.0.0-www-classic-fef33f20";
5689+
exports.version = "19.0.0-www-classic-a5b1d991";

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,10 @@ function pushLinkImpl(target, props) {
704704
target.push("/>");
705705
return null;
706706
}
707+
var styleRegex = /(<\/|<)(s)(tyle)/gi;
708+
function styleReplacer(match, prefix, s, suffix) {
709+
return "" + prefix + ("s" === s ? "\\73 " : "\\53 ") + suffix;
710+
}
707711
function pushSelfClosing(target, props, tag) {
708712
target.push(startChunkForTag(tag));
709713
for (var propKey in props)
@@ -1435,7 +1439,7 @@ function pushStartInstance(
14351439
"symbol" !== typeof child &&
14361440
null !== child &&
14371441
void 0 !== child &&
1438-
target$jscomp$0.push(escapeTextForBrowser("" + child));
1442+
target$jscomp$0.push(("" + child).replace(styleRegex, styleReplacer));
14391443
pushInnerHTML(target$jscomp$0, innerHTML$jscomp$4, children$jscomp$5);
14401444
target$jscomp$0.push(endChunkForTag("style"));
14411445
var JSCompiler_inline_result$jscomp$5 = null;
@@ -1484,7 +1488,9 @@ function pushStartInstance(
14841488
"symbol" !== typeof child$jscomp$0 &&
14851489
null !== child$jscomp$0 &&
14861490
void 0 !== child$jscomp$0 &&
1487-
target.push(escapeTextForBrowser("" + child$jscomp$0));
1491+
target.push(
1492+
("" + child$jscomp$0).replace(styleRegex, styleReplacer)
1493+
);
14881494
pushInnerHTML(target, innerHTML$jscomp$5, children$jscomp$6);
14891495
}
14901496
styleQueue$jscomp$0 &&
@@ -5658,4 +5664,4 @@ exports.renderToString = function (children, options) {
56585664
'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'
56595665
);
56605666
};
5661-
exports.version = "19.0.0-www-modern-39d2e934";
5667+
exports.version = "19.0.0-www-modern-5ad75306";

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4921,6 +4921,26 @@ if (__DEV__) {
49214921
target.push(textSeparator);
49224922
}
49234923
}
4924+
/**
4925+
* This escaping function is designed to work with style tag textContent only.
4926+
*
4927+
* While untrusted style content should be made safe before using this api it will
4928+
* ensure that the style cannot be early terminated or never terminated state
4929+
*/
4930+
4931+
function escapeStyleTextContent(styleText) {
4932+
{
4933+
checkHtmlStringCoercion(styleText);
4934+
}
4935+
4936+
return ("" + styleText).replace(styleRegex, styleReplacer);
4937+
}
4938+
4939+
var styleRegex = /(<\/|<)(s)(tyle)/gi;
4940+
4941+
var styleReplacer = function (match, prefix, s, suffix) {
4942+
return "" + prefix + (s === "s" ? "\\73 " : "\\53 ") + suffix;
4943+
};
49244944

49254945
function pushStyleImpl(target, props) {
49264946
target.push(startChunkForTag("style"));
@@ -4965,7 +4985,7 @@ if (__DEV__) {
49654985
child !== undefined
49664986
) {
49674987
// eslint-disable-next-line react-internal/safe-string-coercion
4968-
target.push(stringToChunk(escapeTextForBrowser("" + child)));
4988+
target.push(stringToChunk(escapeStyleTextContent(child)));
49694989
}
49704990

49714991
pushInnerHTML(target, innerHTML, children);
@@ -5010,7 +5030,7 @@ if (__DEV__) {
50105030
child !== undefined
50115031
) {
50125032
// eslint-disable-next-line react-internal/safe-string-coercion
5013-
target.push(stringToChunk(escapeTextForBrowser("" + child)));
5033+
target.push(stringToChunk(escapeStyleTextContent(child)));
50145034
}
50155035

50165036
pushInnerHTML(target, innerHTML, children);

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,10 @@ function pushLinkImpl(target, props) {
686686
target.push("/>");
687687
return null;
688688
}
689+
var styleRegex = /(<\/|<)(s)(tyle)/gi;
690+
function styleReplacer(match, prefix, s, suffix) {
691+
return "" + prefix + ("s" === s ? "\\73 " : "\\53 ") + suffix;
692+
}
689693
function pushSelfClosing(target, props, tag) {
690694
target.push(startChunkForTag(tag));
691695
for (var propKey in props)
@@ -1428,7 +1432,7 @@ function pushStartInstance(
14281432
"symbol" !== typeof child &&
14291433
null !== child &&
14301434
void 0 !== child &&
1431-
target$jscomp$0.push(escapeTextForBrowser("" + child));
1435+
target$jscomp$0.push(("" + child).replace(styleRegex, styleReplacer));
14321436
pushInnerHTML(target$jscomp$0, innerHTML$jscomp$4, children$jscomp$5);
14331437
target$jscomp$0.push(endChunkForTag("style"));
14341438
var JSCompiler_inline_result$jscomp$5 = null;
@@ -1477,7 +1481,9 @@ function pushStartInstance(
14771481
"symbol" !== typeof child$jscomp$0 &&
14781482
null !== child$jscomp$0 &&
14791483
void 0 !== child$jscomp$0 &&
1480-
target.push(escapeTextForBrowser("" + child$jscomp$0));
1484+
target.push(
1485+
("" + child$jscomp$0).replace(styleRegex, styleReplacer)
1486+
);
14811487
pushInnerHTML(target, innerHTML$jscomp$5, children$jscomp$6);
14821488
}
14831489
styleQueue$jscomp$0 &&

0 commit comments

Comments
 (0)