fix: Track HTML Stack Mutations in Foreign Content Context - #462
Merged
Conversation
The context tracker assumed that an integration point between the current node and the tracked bottom blocks every HTML end tag. That holds for end tags searched in the default, list-item and button scopes and for the "any other end tag" walk, but table scope is bounded only by html, table and template, so </table>, </td> and the rest of the table structure reach past foreignObject, desc, mi and annotation-xml to a table around the foreign content and close all of it. The tracker kept its foreign state, honored the self-closing flag on the next tag, and a dropped element such as <object/> no longer held the text after it. The HTML end-tag scan also popped through elements in the special category, where browsers ignore the tag, matched headings only by exact name although </h1> closes any open heading, popped the elements above a form on </form>, which only removes the form, and cleared its stack on a stray end tag although the browser may still be in foreign content, so the next <svg> inside MathML was taken for an SVG root. The tracker now enters its existing unknown state, where only <svg/> and <math/> close themselves, whenever an end tag reaches the HTML rules with an effect that depends on untracked ancestors, on the insertion mode, or on the list of active formatting elements: table-scope and template end tags, formatting end tags across a special element, stray end tags with nothing tracked in the way, and table, template, select and frameset start tags at an integration point. Within the tracked region the walk follows the spec: the special category for any other end tag, the scopes for the tags with their own rules, heading cross-matching, and form removal. Integration points and annotation-xml bound every scope. Verified against Chrome on the hand-built cases and on 2000 generated inputs, where the fix removes every divergence toward foreign state. Closes #461. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VYF1WjZmwbGNrph7RDw2BS
Each case was verified against Chrome 152 with <object/>hidden as the oracle: an HTML object owning the text means the browser is in HTML content and the sanitizer must not honor the self-closing flag. - An xmp start tag closes an open p, like pre and listing. - A table start tag after an open p fails closed, since only a no-quirks document closes the p and the embedding document's mode is unknown. - Only the first of duplicate type attributes decides whether an input is hidden, as the tokenizer drops the later ones. - Untracked cells, captions, sections and nested tables are followed with a bounded stack, so a cell or section end tag that names something else is ignored as browsers ignore it, and well-formed nested tables and captions no longer poison later SVG. - A search element makes the context unknown when an end-tag or list-item walk reaches it, since the specification puts it in the special category and current Chrome does not. - A self-closing svg or math root still closes itself inside a tracked empty table when the tracker gives up. Issue #461. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RajvH16nNkgw9TJS6nEpTT
dialog is in the WHATWG parsing algorithm's special category but absent from Chrome's special-node set, the same split the review follow-up already handled for search. A stack walk that reached an open dialog followed Chrome alone, so a later self-closing <object/> (or a raw-text element such as <title/>) was honored, exposing text that a spec-compliant or Firefox parser keeps inside the HTML element. Add dialog to the special and ambiguously-special sets so the three affected walks fail closed: the "any other end tag" scan, the list-item start scan, and the adoption agency's furthest-block search. Add a regression test covering each walk; every case leaks the text on the prior branch head and is suppressed now. A modeled dialog block transition still honors a later self-closing tag. Issue #461. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015c2SFXiJJvU9JfWBCtGdNo
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.
Summary
Fixes #461.
The bounded foreign-content context tracker now models the HTML tree-builder transitions that can change whether a self-closing flag is honored inside SVG or MathML. It follows the current in-body rules for p, headings, list items, buttons, forms, formatting elements, select/option/optgroup,
image, and current tree-builder void elements. It also tracks the form pointer and the inherited in-body, in-table, or in-cell mode across a foreign root.When the correct result still depends on state outside the bounded tracker—table/template state, a complex table transition, or reconstructing the active formatting list—the tracker enters its sticky fail-closed state. In that state only self-closing
<svg/>and<math/>roots close themselves.The element sets now match the current WHATWG rules:
selectis both a default-scope boundary and a specifically handled end tag, but its start tag no longer changes the insertion mode. The tracker also uses a tree-builder-specific immediate-pop set, since Chrome treatscommandandisindexas ordinary HTML elements while still immediately poppingbgsound.Known, well-formed transitions recover precisely: a foreign root in a table cell, a completed
selectisland, and an emptytableisland do not poison later SVG self-closing tags.No shipped release contains the affected context-tracker code.
Chrome oracle cases
Each hostile transition is followed by
<object/>hidden. An uppercase HTMLOBJECTowninghiddenmeans Chrome is in HTML content and the sanitizer must suppress that fallback text.Representative cases fixed here:
The current-select behavior is intentionally different:
Test plan
image, select/option rules, and current parser element categories../mvnw clean verifypasses on JDK 11, 17, 21, and 25 (490 reactor tests per run, including fuzzers and the JPMS consumer check).Review follow-up (f9819fa)
An independent review against the current WHATWG tree-construction rules and Chrome 152 found five cases where the tracker stayed in foreign content while Chrome was in HTML content, plus one avoidable fidelity loss. Each was reproduced with
<object/>hiddenas the oracle and is fixed with a regression test:<xmp>now closes an openp, likepreandlisting:<svg><foreignObject><p><xmp></xmp><math></svg><object/>hidden<table>after an openpfails closed, since only a no-quirks document closes thepand the embedding document's mode is unknown:<svg><foreignObject><p><table></table></foreignObject><object/>hiddentypeattributes decides whether aninputis hidden, as the tokenizer drops the later ones:<table><svg><foreignObject><select><input type=text type=hidden></foreignObject></svg></select></foreignObject><object/>hidden</td>inside ath,</tbody>underthead, or</caption>inside a cell is ignored as browsers ignore it:<table><tr><th></td><svg><foreignObject><form></foreignObject><object/>hiddensearchis in the specification's special category but not in Chrome's, so an end-tag or list-item walk that reaches one fails closed:<svg><foreignObject><cite><search><span></cite></foreignObject></svg></span></search></cite></foreignObject><object/>hidden<table><tr><td><table><tr><td>x</td></tr></table></td></tr></table><svg><path/><rect/></svg>keeps its self-closing tags.A positive test also checks that each modeled transition still honors a later
<path/>, so the transitions are tracked rather than failed closed. Cross-checked with 438 element-category probes and about 78,000 random differential cases in Chrome 152, in quirks and no-quirks mode: zero cases where the branch exposes text that Chrome keeps inside an HTML object../mvnw clean verifypasses on JDK 21 (495 reactor tests).🤖 Generated with Claude Code
https://claude.ai/code/session_01RajvH16nNkgw9TJS6nEpTT