Repository navigation
Don't escape script and style text in inner_html - #334
Merged
Merged
Conversation
inner_html() serialized with ChildrenOnly(None), so html5ever saw the children's text without a parent and escaped it, even inside <script>, <style> and the other raw text elements, unlike html() and inner_html() on the parent. Pass the element's name as the parent instead (only for HTML elements, as an SVG <style> is escaped) and skip the root for any ChildrenOnly scope.
kdrag0n
force-pushed
the
inner-html-raw-text
branch
from
November 11, 2026 14:34
76e16d9 to
ade5b46
Compare
adamreichold
approved these changes
Nov 11, 2026
adamreichold
left a comment
Member
There was a problem hiding this comment.
LGTM.
Please try to reign in your model's cover letters. Commit message would have been sufficient instead of the wall of text.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
inner_html()on a<script>or<style>element returns its text HTML-escaped, so JSON or CSS read that way is corrupted.html()on the same element andinner_html()on the parent return it as is:Before:
After:
The same happens for every raw text element (
xmp,iframe,noembed,noframes,plaintext, andnoscriptnow that scripting is enabled).html5ever's serializer already leaves text in those elements unescaped, but it decides that from the parent on its stack, and when only the children are serialized the bottom of the stack is whatever name is passed in
TraversalScope::ChildrenOnly:https://github.com/servo/html5ever/blob/d7232d746d3112f08926d169591576bd12fe2fdf/html5ever/src/serialize/mod.rs#L248-L260
https://github.com/servo/html5ever/blob/d7232d746d3112f08926d169591576bd12fe2fdf/html5ever/src/serialize/mod.rs#L78-L82
inner_html()always passesNone, so the text has no parent and gets escaped:scraper/scraper/src/element_ref/mod.rs
Lines 71 to 74 in 1506707
This passes the element's name instead, so html5ever applies the same rules as for
html(). Two details:node/serializable.rsonly skipped the root forChildrenOnly(None), so it now does that for anyChildrenOnly. Otherwise the element's own tags would end up in the output (this also affects anyone passingChildrenOnly(Some(_))to html5ever'sserializewith anElementReforHtml).The name is only passed for HTML elements. When html5ever pushes an element it only records the name of HTML elements, but it doesn't check the namespace of the scope's name, so passing an SVG
<style>would leave its text unescaped. Chromium and Firefox escape it.https://github.com/servo/html5ever/blob/d7232d746d3112f08926d169591576bd12fe2fdf/html5ever/src/serialize/mod.rs#L158-L161
I compared the results with
innerHTMLin Chromium 158 and Firefox 159 for all of the raw text elements,title/textarea/p, attribute values, nested elements and SVGstyle/script, and they match. The new tests inelement_ref/mod.rscover the same cases; the raw text and nested ones fail without the change.cargo test --all-features,cargo clippy --all-targets --all-features -- --deny warningsandcargo fmt -- --checkpass (rustc 1.99.1, Linux x86-64), and the commit applies cleanly on top of #332 and passes its lints too.