Repository navigation
Keep the list's implied item out of a forwarded foreign root - #495
Merged
Merged
Conversation
The containment metadata implies a list item for a list's content. A forwarded SVG or MathML root has no entry on the balancer's stack, so text or a foreign child inside it was judged against the list below and the item was emitted inside the root. An li is a breakout name: a browser popped the root at the item and read the SVG textarea or a that followed as an HTML element, so <ul><svg><textarea>x</textarea> </svg></ul> came back with an HTML textarea. A start tag that is a foreign element in the input and in the output now skips HTML containment, as one at an integration point already did. For text, and for an element with an HTML entry open above the root, the root is the boundary: the entries below it imply nothing and closing stops there. Formatting still resumes, as a browser reconstructs it at an integration point. The policy's text gate judged such content only through that item. Without it, text in a foreign tbody was dropped by the HTML rule that a table part holds no text. An element the output parser inserts as SVG or MathML now holds text unless the author disallowed text in that name; a literal-content name such as style keeps its bar. Part of #492 (item 1). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Below the root the HTML entries implied nothing at all, which also dropped the wrappers a list item or an option is never emitted without: an option that closed the table structure above the root in an integration point came out bare, and the next pass gave it its select. The probe found that on one shape in each policy that keeps options. Judge such content against the body instead, so the free wrappers still come and the list's item still does not. Part of #492 (item 1). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The boundary was the innermost forwarded root, judged inside whenever an HTML entry was open above it. That was also true after a breakout element with a stack entry had popped the root in both parsers, so the closing loop stopped short and a link below the root was no longer ended by a new link: <a><svg><div>x<a>y came back with the second link inside the first, and the next pass moved it out. The root is now the innermost forwarded one that the output parser entered and the input parser still has open, asked by the identity the tracker gave its node, so a breakout out of an inner root moves the boundary to the next root out and one out of the last root removes it. When the input parser has none open but the output parser still does, because the policy dropped the breakout, the innermost emitted root stands. The link scan that ends an open link for a new one looks no further down than the boundary: a link inside an integration point is out of the outer link's scope, as it is for the adoption agency algorithm, so the formatting element around it is no longer closed and reopened empty. A table part at an integration point the policy dropped is HTML in the input, where a browser ignores it, but its output sits in the foreign root, where an implied HTML table would break out and turn every following SVG or MathML sibling into HTML. It is forwarded as the foreign element the output parser makes of it, as a part under foreign rules already was. Tests cover the links around breakouts and integration points, the table parts, and the exact set of names whose text the gate now keeps as foreign elements: the five table-part names, enumerated from the containment metadata. Part of #492 (item 1). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A foreign element nested in an SVG textArea that a policy writes out in lower case is read back as the textarea's text by this receiver's lexer, which reads that name as raw text in any namespace, so the next pass escaped it. A start tag under foreign rules keeps the HTML containment that closes such a node first, as before. Part of #492 (item 1). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…foreign option A table part under an integration point the policy dropped is foreign in the output, so it is forwarded as that foreign element instead of being given an HTML table that breaks out of the root; its stray end tag is routed through the input tracker's end-tag rule so the tracker keeps its context for the siblings. An option or optgroup that is itself an SVG or MathML element gets no select, as a browser gives it none. Tests cover the forwarded parts' siblings, nested roots, and foreign options. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s a root, outside raw-text nodes The forwarding of a table part as the foreign element its output would be is again conditioned on the input: the part is under foreign rules there, or the input tracker still names an open foreign root, which it does at an integration point and under HTML inside one, or a table return or pushed outputless table is pending, as before. Once the input parser's context is unknown the part takes the table path, as on main, and so does a part in a node this receiver's lexer reads as raw text, a recased SVG textArea, which HTML containment closes first. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…der a known root A start tag the output parser inserts as a foreign element skips HTML containment whenever the input parser still names a foreign root, so an HTML-named element at an integration point the policy dropped, or inside a forwarded table part, no longer gets the list's item; an unknown input context keeps HTML containment, as the forms test requires. The forwarded part's end tag is processed as usual when a foreign node of its name is open further out, which the foreign end-tag algorithm pops through the integration point. The text gate's foreign-element rule asks the retained template's context when one is open. The link-ending rule is one helper again and the unbounded overload is gone. The new tests check namespaces in the output's browser tree instead of parsing the expected string twice. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.
Item 1 of #492: an implied
<li>lands inside a foreign root and flips a child's namespace.What broke
With a policy allowing
ul,li,svgandtextarea,<ul><svg><textarea>x</textarea></svg></ul>came out as<ul><svg><li><textarea>x</textarea></li></svg></ul>. The browser tree of that output isul > svg > li > textareawith an HTMLtextarea:liis a breakout name, so the browser pops thesvgat the item and everything after it is HTML. The input's tree isul > svg > svg:textarea. Same for<a>, for<mi>and<mtext>undermath, for bare text in the root (<ul><svg>x</svg></ul>gave<ul><svg><li>x</li></svg></ul>), and for an SVGtr/td. An SVGainside an HTML link closed the link (<a><svg><a>x</a></svg></a>gave<a><svg></svg></a><a>x</a>). Every one of these was correct under a policy that dropsli, because the item was then dropped and its text landed in the right place.Why
The containment metadata implies a list item for a list's content. A forwarded SVG or MathML root has no entry on the balancer's stack, so the text or foreign child inside it was judged against the
ulbelow and the item was opened, and emitted, inside the root. A start tag at an HTML integration point already skipped that containment; a start tag that is a foreign element did not, and text never did.The issue's caveat held too: with the item gone,
<ul><svg><tbody>DlostD, because the policy's text gate judged the foreigntbodyby its HTML name, whose default is "no text" since a browser foster-parents text out of a table part.<svg><tbody>DdroppedDunder every policy on main; only the item had kept it under a list.What changed
TagBalancingHtmlStreamEventReceiver: a start tag that the output parser inserts as an SVG or MathML element skips HTML containment, exactly as one at an integration point does (theformclause there was a special case of this), as long as the input parser still names a foreign root: the output decides, so an HTML-named element at an integration point the policy dropped, or inside a forwarded table part, is foreign too and gets no list item. An unknown input context keeps HTML containment (a form under a root still closes the form before it), and so does a foreign node this receiver's lexer reads as raw text, an SVGtextAreaa policy writes in lower case, where elements nested inside would be read back as text. For text, and for an element with an HTML entry open above the root (HTML inside an integration point), the root is a boundary: the entries below it imply nothing, the content is judged as in a fresh body, so anoptionorlistill gets theselectorulit is never emitted without, and the container-closing loop stops there. The boundary is the innermost forwarded root that the output parser entered and the input parser still has open, asked through a newForeignContentContext.isNodeOpen(serial)query, so a breakout out of an inner root moves it to the next root out and one out of the last root removes it; when the input parser has none open but the output still does, because the policy dropped the breakout, the innermost emitted root stands. The link scan that ends an open link for a new one looks no further down than the boundary, since a link inside an integration point is out of the outer link's scope for the adoption agency algorithm. Formatting still resumes, as a browser reconstructs it at an integration point. A table part whose output would be foreign is forwarded as the foreign element the output parser makes of it while the input parser still has a foreign root open, which it does at an integration point the policy dropped and under HTML inside one, as a part under foreign rules already was, instead of implying an HTML table that would break out of the root in the output and turn every sibling after it into HTML; once the input parser's context is unknown the part takes the table path as before, and so does one inside a recased SVGtextArea, which this lexer reads as raw text on the next pass. The forwarded part's stray end tag, which the input parser ignores, is told to the input tracker as such instead of making it give up its context, so the foreign siblings after it stay foreign; when a foreign node of that name is open further out, which the foreign end-tag algorithm pops through the integration point, the end tag is processed as usual. Anoptionoroptgroupthat is itself an SVG or MathML element gets noselect, since it is not an HTML option; one at an integration point still does. Breakouts are otherwise untouched: the browser pops the root for them and they are the list's content again.ElementAndAttributePolicyBasedSanitizerPolicy: an element the output parser inserts as SVG or MathML holds text unless the author disallowed text in that name, asking the retained template's context when one is open, as every other output namespace decision there does. Only the table-part names change hands in practice (table,thead,tbody,tfoot,tr,colgroup); a literal-content name such asstyleorscriptkeeps its bar, and the prepackaged policies allow no foreign root, so nothing inSanitizerswidens.HtmlSanitizerTest: the reproductions and neighbouring shapes, each a fixed point whose output parses to the browser tree of the input with namespaces; hostile payloads; the gate positive and negative (disallowTextIn,style, an HTMLtbody); a droppedtbody, a dropped root, and the change listener, which no longer sees a discardedlithe input never had. From the reviews: links around breakouts and inside integration points, a breakout out of an inner root only and one directly in a nested root, table parts under a dropped integration point and under HTML inside one with foreign siblings after them (a link, atextareaholding markup, a hostile payload), foreign options and an integration-point option, and the exact set of names whose text the gate now keeps as foreign elements, enumerated from the containment metadata:colgroup,tbody,tfoot,theadandtr, undersvgand undermath. Where an output differs from its input, the tests compare browser trees with namespaces, or assert that the elements in question are SVG or MathML elements, and never HTML ones, in the output's browser tree; an unchanged output needs no tree comparison.change_log.mdbullet.Before and after
Policy:
ul,ol,li,svg,math,textarea,a,mi,tbody,b,p,foreignObject,desc,path,div.<ul><svg><textarea>x</textarea></svg></ul><ul><svg><li><textarea>x</textarea></li></svg></ul>(HTML textarea)<ul><svg><a>x</a></svg></ul><ul><svg><li><a>x</a></li></svg></ul>(HTML a)<ul><math><mi>x</mi></math></ul><ul><math><mi><li>x</li></mi></math></ul><ul><svg><tbody>D<ul><svg><tbody><li>D</li></tbody></svg></ul><ul><svg><tbody>D</tbody></svg></ul><ul><svg>x</svg></ul><ul><svg><li>x</li></svg></ul><a><svg><a>x</a></svg></a><a><svg></svg></a><a>x</a><ul><svg><foreignObject><p>a<div>b...<p>a</p><li><div>b</div></li>......<p>a</p><div>b</div>...<svg><tbody>D(any policy allowing both)<svg><tbody></tbody></svg><svg><tbody>D</tbody></svg><ul><svg><foreignObject><tbody>x</tbody></foreignObject><a>y</a></svg></ul>,foreignObjectdropped<ul><svg><table><tbody>x</tbody></table><li><a>y</a></li></svg></ul>(HTML table and link)<ul><svg><tbody>x</tbody><a>y</a></svg></ul><svg><option>x</option></svg><svg><select><option>x</option></select></svg><ul><svg><foreignObject><textarea>x</textarea></foreignObject><a>y</a></svg></ul>,foreignObjectdropped<ul><svg><li><textarea>x</textarea></li><li><a>y</a></li></svg></ul><ul><svg><textarea>x</textarea><a>y</a></svg></ul>Every branch output parses to the input's tree, namespaces included. Under a policy that drops
li, outputs are byte-identical to main except the foreigntbodytext. UnderSanitizers.BLOCKS, which allows nosvg, outputs are unchanged.Verification
./mvnw -o -ntp -B clean verifyon the final commitec40ba1: build success, 683 tests, 0 failures (679 existing, unchanged, plus 4 new).<ul><svg>x</svg></ul>and<ul><svg><a>x</a></svg></ul>nested 253 and 254 deep are fixed points that parse to the input's tree; one deeper the root does not fit and the text flattens, as before.probes-review3/ReviewProbe, 15,430 inputs x 44 policies,sanandeventsmodes, maine740164against the final commitec40ba1):san: 678,920 rows compared. 6,782 differ in output. 357 rows lose every flag (the foreigntbody,trandcolgrouptext kept, and first-pass structure), 60 keep fewer flags, and 56 rows on 9 distinct inputs gain one.text-changed, 34 rows on 3 inputs:<style>written inside ansvginside aselect, which now stays inside thesvg, where the CVE-2021-42575 rule that astyleinside aselectholds no text closes it at once, so its raw text comes out as escaped text beside it; main moved thestyleout of theselectand kept the raw text as a stylesheet. The third is<Svg><button><style>under a policy allowing text instyle, where the stylesheet stays in the SVGstyleelement instead of an HTML one.text-lost, 3 rows on 2 inputs: the recase policies on an input whoseforeignObjectthey drop, where astylein an option at the dropped point is now the SVGstyleelement the output parser makes of it, holding the stylesheet as its text, escaped as the renderer escapes everything insidesvg; main emptied an HTMLstyleinside an impliedselectunder the CVE-2021-42575 rule and wrote the text beside it, and the probe counts stylesheet text as lost. Andtablerenamed todivon an input where a list written inside a root inside aselectnow stays inside the root, where the input has it, and there main's handling of that rename under a list in a select drops the cell's text, as it does for<select><svg><ul><span><tr><td>tailon main; main had moved the list out of the root.xmptopre, a breakout name, inside a foreign root (11 rows, one input): the first pass keeps main's structure and the second sees the breakout; (b) the recase policies on an input whoseforeignObjectunder a nested root they drop (4 rows): the input'sstrongafter it is HTML at the integration point, the output's breaks out of both roots into the list, and the second pass gives it the list's item, as a breakout in a root gets; (c)tablerenamed todiv(2 rows): anoptgroupwritten while aselectthe input still has open is closed in the output gets noselectuntil the second pass, which is the wrapper judgment Give a free option or list item its wrapper under every container, and push the select out of a kept table #497 (item 9) reworks; (d) the two policies that droptable(2 rows): text after an integration point inside a forwarded foreign row, which main dropped altogether, is kept inside the row on the first pass and after it on the second, where the inner table's dropped parts are gone.events: 36,063 rows, the receiver's own event column identical on every row, no row gains a flag.Review
/code-reviewon the first commit found one regression and three gaps in the boundary: after a breakout element with its own stack entry the entries above the root are HTML flow again, so a link below the root was no longer ended by a new link (<a><svg><div>x<a>ycame back nested); the link scan reached below the boundary and closed and reopened a formatting element at an integration point, leaving an empty<b></b>; a table part at an integration point the policy dropped implied an HTML table inside the output's foreign root; and content below the boundary implied nothing at all, which also dropped theselectanoptionmust never be emitted without. All four are fixed above, each with a test. The review's count of the gate's widening (addinghtml,frame,frameset,bgsound) did not hold: those come out empty or beside the text under a foreign root on main too, and the enumeration test pins the five table-part names. Its suggestion to mark forwarded roots once instead of re-deriving them is left for the follow-up that reworks the passthrough helpers together.A second
/code-reviewon the reviewed commit found the boundary wrong for a breakout out of an inner root only, which should land at the integration point inside the outer root rather than below it; an HTML table implied for a part under an HTML element inside a dropped integration point, which the first fix had covered only directly under the point; the forwarded part's stray end tag making the input tracker give up its context, so the next foreign sibling got the list's item back; and aselectimplied for an SVGoption. All four are fixed above, each with a test. Its suggestion to trust the output tracker alone once the input tracker is unknown was tried and reverted: it broke the existing test that keeps foreign forms after an unknown context out of the form element pointer, and the probe showed it forwarding parts under stray end tags where main's table path was right, so the forwarding is conditioned on the input naming a root instead.A third
/code-reviewfound the item's symptom surviving for an HTML-named element at an integration point the policy dropped, and inside a forwarded part (<ul><svg><foreignObject><textarea>x</textarea></foreignObject><a>y</a></svg></ul>still got two list items; theoptionvariant was no longer a fixed point): the containment skip required the input to be foreign too. It now follows the output, under a known input root, with tests for the three shapes. The review also found the forwarded part's end-tag branch ignoring an end tag that the foreign end-tag algorithm carries through the integration point to an outer foreign node of the same name, now guarded as the sibling branch is and tested; the text gate asking the outer output tracker inside retained template contents, now the template's; the browser-tree assertions in the new tests parsing the expected string twice and ignoring namespaces, now replaced; a dead overload, an orphaned javadoc and a rule inlined twice, all tidied. Its performance note, that the boundary is recomputed per event, is the follow-up already mentioned.Left out
<ul><svg><b>x</b></svg></ul>) still gets the item, lexically inside the root:<ul><svg><li><b>x</b></li></svg></ul>. A browser pops the root at thelias it does at theb, and the result is the list normalisation the sanitizer applies to<ul><b>x</b></ul>. Unchanged here.<ul><svg><a>x<li>y</li></a></svg></ul>) is judged against that entry, so an extra<ul>is implied inside the SVGa; the browser pops theawith the root and puts the item in the outer list. Main produced the same tree by another route. Popping foreign stack entries on a breakout is its own change.<ul><svg><foreignObject><tr><td>x, with the integration point kept, still closes the list and the root and emits the table after them, as on main: table parts keep the table path.PolicyFactoryis unaffected.<textarea>as RCDATA inside<svg>too, where a browser reads markup; the content comes out escaped, so it fails closed. Not this item.Part of #492 (item 1)
🤖 Generated with Claude Code