Commit ea2a538e authored by Jan Köster's avatar Jan Köster
Browse files

test

parent 1c26a74e
Loading
Loading
Loading
Loading
+63 −2
Original line number Diff line number Diff line
@@ -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 "";
@@ -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;
}