Skip to content

Commit

Permalink
[Fizz] escape <style> textContent as css (#28870)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
gnoff committed Apr 19, 2024
1 parent f29aad9 commit e7ed683
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 17 deletions.
2 changes: 1 addition & 1 deletion compiled/facebook-www/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4c34a7ffc59207e8baa19e8c4698a2f90a46177c
aead514db2808a2e82c128aa4db459939ab88b58
26 changes: 23 additions & 3 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-9835bfc2";
var ReactVersion = "19.0.0-www-classic-8765e454";

// This refers to a WWW module.
var warningWWW = require("warning");
Expand Down Expand Up @@ -4924,6 +4924,26 @@ if (__DEV__) {
target.push(textSeparator);
}
}
/**
* This escaping function is designed to work with style tag textContent only.
*
* While untrusted style content should be made safe before using this api it will
* ensure that the style cannot be early terminated or never terminated state
*/

function escapeStyleTextContent(styleText) {
{
checkHtmlStringCoercion(styleText);
}

return ("" + styleText).replace(styleRegex, styleReplacer);
}

var styleRegex = /(<\/|<)(s)(tyle)/gi;

var styleReplacer = function (match, prefix, s, suffix) {
return "" + prefix + (s === "s" ? "\\73 " : "\\53 ") + suffix;
};

function pushStyleImpl(target, props) {
target.push(startChunkForTag("style"));
Expand Down Expand Up @@ -4968,7 +4988,7 @@ if (__DEV__) {
child !== undefined
) {
// eslint-disable-next-line react-internal/safe-string-coercion
target.push(stringToChunk(escapeTextForBrowser("" + child)));
target.push(stringToChunk(escapeStyleTextContent(child)));
}

pushInnerHTML(target, innerHTML, children);
Expand Down Expand Up @@ -5013,7 +5033,7 @@ if (__DEV__) {
child !== undefined
) {
// eslint-disable-next-line react-internal/safe-string-coercion
target.push(stringToChunk(escapeTextForBrowser("" + child)));
target.push(stringToChunk(escapeStyleTextContent(child)));
}

pushInnerHTML(target, innerHTML, children);
Expand Down
26 changes: 23 additions & 3 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-72ca4dea";
var ReactVersion = "19.0.0-www-modern-e48dd4e1";

// This refers to a WWW module.
var warningWWW = require("warning");
Expand Down Expand Up @@ -4924,6 +4924,26 @@ if (__DEV__) {
target.push(textSeparator);
}
}
/**
* This escaping function is designed to work with style tag textContent only.
*
* While untrusted style content should be made safe before using this api it will
* ensure that the style cannot be early terminated or never terminated state
*/

function escapeStyleTextContent(styleText) {
{
checkHtmlStringCoercion(styleText);
}

return ("" + styleText).replace(styleRegex, styleReplacer);
}

var styleRegex = /(<\/|<)(s)(tyle)/gi;

var styleReplacer = function (match, prefix, s, suffix) {
return "" + prefix + (s === "s" ? "\\73 " : "\\53 ") + suffix;
};

function pushStyleImpl(target, props) {
target.push(startChunkForTag("style"));
Expand Down Expand Up @@ -4968,7 +4988,7 @@ if (__DEV__) {
child !== undefined
) {
// eslint-disable-next-line react-internal/safe-string-coercion
target.push(stringToChunk(escapeTextForBrowser("" + child)));
target.push(stringToChunk(escapeStyleTextContent(child)));
}

pushInnerHTML(target, innerHTML, children);
Expand Down Expand Up @@ -5013,7 +5033,7 @@ if (__DEV__) {
child !== undefined
) {
// eslint-disable-next-line react-internal/safe-string-coercion
target.push(stringToChunk(escapeTextForBrowser("" + child)));
target.push(stringToChunk(escapeStyleTextContent(child)));
}

pushInnerHTML(target, innerHTML, children);
Expand Down
12 changes: 9 additions & 3 deletions compiled/facebook-www/ReactDOMServer-prod.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,10 @@ function pushLinkImpl(target, props) {
target.push("/>");
return null;
}
var styleRegex = /(<\/|<)(s)(tyle)/gi;
function styleReplacer(match, prefix, s, suffix) {
return "" + prefix + ("s" === s ? "\\73 " : "\\53 ") + suffix;
}
function pushSelfClosing(target, props, tag) {
target.push(startChunkForTag(tag));
for (var propKey in props)
Expand Down Expand Up @@ -1435,7 +1439,7 @@ function pushStartInstance(
"symbol" !== typeof child &&
null !== child &&
void 0 !== child &&
target$jscomp$0.push(escapeTextForBrowser("" + child));
target$jscomp$0.push(("" + child).replace(styleRegex, styleReplacer));
pushInnerHTML(target$jscomp$0, innerHTML$jscomp$4, children$jscomp$5);
target$jscomp$0.push(endChunkForTag("style"));
var JSCompiler_inline_result$jscomp$5 = null;
Expand Down Expand Up @@ -1484,7 +1488,9 @@ function pushStartInstance(
"symbol" !== typeof child$jscomp$0 &&
null !== child$jscomp$0 &&
void 0 !== child$jscomp$0 &&
target.push(escapeTextForBrowser("" + child$jscomp$0));
target.push(
("" + child$jscomp$0).replace(styleRegex, styleReplacer)
);
pushInnerHTML(target, innerHTML$jscomp$5, children$jscomp$6);
}
styleQueue$jscomp$0 &&
Expand Down Expand Up @@ -5680,4 +5686,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-fef33f20";
exports.version = "19.0.0-www-classic-a5b1d991";
12 changes: 9 additions & 3 deletions compiled/facebook-www/ReactDOMServer-prod.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,10 @@ function pushLinkImpl(target, props) {
target.push("/>");
return null;
}
var styleRegex = /(<\/|<)(s)(tyle)/gi;
function styleReplacer(match, prefix, s, suffix) {
return "" + prefix + ("s" === s ? "\\73 " : "\\53 ") + suffix;
}
function pushSelfClosing(target, props, tag) {
target.push(startChunkForTag(tag));
for (var propKey in props)
Expand Down Expand Up @@ -1435,7 +1439,7 @@ function pushStartInstance(
"symbol" !== typeof child &&
null !== child &&
void 0 !== child &&
target$jscomp$0.push(escapeTextForBrowser("" + child));
target$jscomp$0.push(("" + child).replace(styleRegex, styleReplacer));
pushInnerHTML(target$jscomp$0, innerHTML$jscomp$4, children$jscomp$5);
target$jscomp$0.push(endChunkForTag("style"));
var JSCompiler_inline_result$jscomp$5 = null;
Expand Down Expand Up @@ -1484,7 +1488,9 @@ function pushStartInstance(
"symbol" !== typeof child$jscomp$0 &&
null !== child$jscomp$0 &&
void 0 !== child$jscomp$0 &&
target.push(escapeTextForBrowser("" + child$jscomp$0));
target.push(
("" + child$jscomp$0).replace(styleRegex, styleReplacer)
);
pushInnerHTML(target, innerHTML$jscomp$5, children$jscomp$6);
}
styleQueue$jscomp$0 &&
Expand Down Expand Up @@ -5658,4 +5664,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-39d2e934";
exports.version = "19.0.0-www-modern-5ad75306";
24 changes: 22 additions & 2 deletions compiled/facebook-www/ReactDOMServerStreaming-dev.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -4921,6 +4921,26 @@ if (__DEV__) {
target.push(textSeparator);
}
}
/**
* This escaping function is designed to work with style tag textContent only.
*
* While untrusted style content should be made safe before using this api it will
* ensure that the style cannot be early terminated or never terminated state
*/

function escapeStyleTextContent(styleText) {
{
checkHtmlStringCoercion(styleText);
}

return ("" + styleText).replace(styleRegex, styleReplacer);
}

var styleRegex = /(<\/|<)(s)(tyle)/gi;

var styleReplacer = function (match, prefix, s, suffix) {
return "" + prefix + (s === "s" ? "\\73 " : "\\53 ") + suffix;
};

function pushStyleImpl(target, props) {
target.push(startChunkForTag("style"));
Expand Down Expand Up @@ -4965,7 +4985,7 @@ if (__DEV__) {
child !== undefined
) {
// eslint-disable-next-line react-internal/safe-string-coercion
target.push(stringToChunk(escapeTextForBrowser("" + child)));
target.push(stringToChunk(escapeStyleTextContent(child)));
}

pushInnerHTML(target, innerHTML, children);
Expand Down Expand Up @@ -5010,7 +5030,7 @@ if (__DEV__) {
child !== undefined
) {
// eslint-disable-next-line react-internal/safe-string-coercion
target.push(stringToChunk(escapeTextForBrowser("" + child)));
target.push(stringToChunk(escapeStyleTextContent(child)));
}

pushInnerHTML(target, innerHTML, children);
Expand Down
10 changes: 8 additions & 2 deletions compiled/facebook-www/ReactDOMServerStreaming-prod.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,10 @@ function pushLinkImpl(target, props) {
target.push("/>");
return null;
}
var styleRegex = /(<\/|<)(s)(tyle)/gi;
function styleReplacer(match, prefix, s, suffix) {
return "" + prefix + ("s" === s ? "\\73 " : "\\53 ") + suffix;
}
function pushSelfClosing(target, props, tag) {
target.push(startChunkForTag(tag));
for (var propKey in props)
Expand Down Expand Up @@ -1428,7 +1432,7 @@ function pushStartInstance(
"symbol" !== typeof child &&
null !== child &&
void 0 !== child &&
target$jscomp$0.push(escapeTextForBrowser("" + child));
target$jscomp$0.push(("" + child).replace(styleRegex, styleReplacer));
pushInnerHTML(target$jscomp$0, innerHTML$jscomp$4, children$jscomp$5);
target$jscomp$0.push(endChunkForTag("style"));
var JSCompiler_inline_result$jscomp$5 = null;
Expand Down Expand Up @@ -1477,7 +1481,9 @@ function pushStartInstance(
"symbol" !== typeof child$jscomp$0 &&
null !== child$jscomp$0 &&
void 0 !== child$jscomp$0 &&
target.push(escapeTextForBrowser("" + child$jscomp$0));
target.push(
("" + child$jscomp$0).replace(styleRegex, styleReplacer)
);
pushInnerHTML(target, innerHTML$jscomp$5, children$jscomp$6);
}
styleQueue$jscomp$0 &&
Expand Down

0 comments on commit e7ed683

Please sign in to comment.