Loading editor/src/webedit_api.cpp +63 −2 Original line number Diff line number Diff line Loading @@ -6062,6 +6062,67 @@ json_object *webedit::Api::serializeTree(const blogi::webedit::EditPlugin *node) return obj; } namespace { // Breaks up every occurrence of @p closingTag (e.g. "</script", "</style"), // matched case-insensitively, by inserting a backslash right after the '<' // -- so a bundle of fetched JS/CSS that happens to contain that sequence // inside a string literal or comment (common in any real-world script that // itself does HTML/template building) can't prematurely close the // surrounding <script>/<style> element once concatenated into one blob and // embedded via renderTree below. The HTML tokenizer scans for that literal // byte sequence with no awareness of JS/CSS syntax, so on a real page- // builder site's ~2.7MB of concatenated bundle JS, a single "</script>" 3+ // megabytes in (e.g. inside a minified HTML-sanitizer's own source) split // the element there: everything after it fell out of the <script> tag // entirely and was parsed as ordinary (invisible, inert) HTML instead of // executing, taking the whole imported page's runtime behavior down with // it. The inserted backslash is a no-op in both languages wherever this // text can legally appear -- inside a JS/CSS string it's an escaped '/' // (same character), inside a comment it's inert text either way -- and // "<" immediately followed by "/script"/"/style" essentially never occurs // as meaningful bare syntax outside those two contexts. std::string breakClosingTag(const std::string &text, const std::string &closingTag) { // Matched against a lowercased copy (closingTag is passed in already // lowercase) so "</SCRIPT"/"</Script" are caught exactly like // "</script" -- browsers close the element on any casing, scanning raw // bytes with no awareness of JS/CSS syntax. Spliced into the ORIGINAL // (mixed-case) text, though: this only needs to break the literal byte // match, not normalize case, and rewriting a JS/CSS identifier's case // could otherwise corrupt it. std::string lower = text; std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); }); std::string out; out.reserve(text.size()); size_t pos = 0; while (pos < text.size()) { if (lower.compare(pos, closingTag.size(), closingTag) == 0) { out += text[pos]; // '<' out += '\\'; out.append(text, pos + 1, closingTag.size() - 1); pos += closingTag.size(); continue; } out += text[pos]; ++pos; } return out; } std::string escapeInlineScript(const std::string &js) { if (js.find_first_of("<") == std::string::npos) return js; return breakClosingTag(js, "</script"); } std::string escapeInlineStyle(const std::string &css) { if (css.find_first_of("<") == std::string::npos) return css; return breakClosingTag(css, "</style"); } } // namespace std::string webedit::Api::renderTree(const blogi::webedit::EditPlugin *node, const std::string &extraCss, const std::string &extraJs) { if (!node) return ""; Loading Loading @@ -6102,10 +6163,10 @@ std::string webedit::Api::renderTree(const blogi::webedit::EditPlugin *node, con libhtmlpp::print(wrapper, hs, false); std::string html = hs.str(); if (!extraCss.empty()) { html = "<style>" + extraCss + "</style>" + html; html = "<style>" + escapeInlineStyle(extraCss) + "</style>" + html; } if (!extraJs.empty()) { html += "<script>" + extraJs + "</script>"; html += "<script>" + escapeInlineScript(extraJs) + "</script>"; } return html; } Loading Loading
editor/src/webedit_api.cpp +63 −2 Original line number Diff line number Diff line Loading @@ -6062,6 +6062,67 @@ json_object *webedit::Api::serializeTree(const blogi::webedit::EditPlugin *node) return obj; } namespace { // Breaks up every occurrence of @p closingTag (e.g. "</script", "</style"), // matched case-insensitively, by inserting a backslash right after the '<' // -- so a bundle of fetched JS/CSS that happens to contain that sequence // inside a string literal or comment (common in any real-world script that // itself does HTML/template building) can't prematurely close the // surrounding <script>/<style> element once concatenated into one blob and // embedded via renderTree below. The HTML tokenizer scans for that literal // byte sequence with no awareness of JS/CSS syntax, so on a real page- // builder site's ~2.7MB of concatenated bundle JS, a single "</script>" 3+ // megabytes in (e.g. inside a minified HTML-sanitizer's own source) split // the element there: everything after it fell out of the <script> tag // entirely and was parsed as ordinary (invisible, inert) HTML instead of // executing, taking the whole imported page's runtime behavior down with // it. The inserted backslash is a no-op in both languages wherever this // text can legally appear -- inside a JS/CSS string it's an escaped '/' // (same character), inside a comment it's inert text either way -- and // "<" immediately followed by "/script"/"/style" essentially never occurs // as meaningful bare syntax outside those two contexts. std::string breakClosingTag(const std::string &text, const std::string &closingTag) { // Matched against a lowercased copy (closingTag is passed in already // lowercase) so "</SCRIPT"/"</Script" are caught exactly like // "</script" -- browsers close the element on any casing, scanning raw // bytes with no awareness of JS/CSS syntax. Spliced into the ORIGINAL // (mixed-case) text, though: this only needs to break the literal byte // match, not normalize case, and rewriting a JS/CSS identifier's case // could otherwise corrupt it. std::string lower = text; std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); }); std::string out; out.reserve(text.size()); size_t pos = 0; while (pos < text.size()) { if (lower.compare(pos, closingTag.size(), closingTag) == 0) { out += text[pos]; // '<' out += '\\'; out.append(text, pos + 1, closingTag.size() - 1); pos += closingTag.size(); continue; } out += text[pos]; ++pos; } return out; } std::string escapeInlineScript(const std::string &js) { if (js.find_first_of("<") == std::string::npos) return js; return breakClosingTag(js, "</script"); } std::string escapeInlineStyle(const std::string &css) { if (css.find_first_of("<") == std::string::npos) return css; return breakClosingTag(css, "</style"); } } // namespace std::string webedit::Api::renderTree(const blogi::webedit::EditPlugin *node, const std::string &extraCss, const std::string &extraJs) { if (!node) return ""; Loading Loading @@ -6102,10 +6163,10 @@ std::string webedit::Api::renderTree(const blogi::webedit::EditPlugin *node, con libhtmlpp::print(wrapper, hs, false); std::string html = hs.str(); if (!extraCss.empty()) { html = "<style>" + extraCss + "</style>" + html; html = "<style>" + escapeInlineStyle(extraCss) + "</style>" + html; } if (!extraJs.empty()) { html += "<script>" + extraJs + "</script>"; html += "<script>" + escapeInlineScript(extraJs) + "</script>"; } return html; } Loading