Close the open list item for a list item start tag, through formatting - #500
Merged
Merged
Conversation
A browser's list item start tag closes the list item that is open before it inserts the new one, walking down its stack past the formatting elements open inside that item, so the items are siblings and the formatting is reconstructed inside the second. The balancer consulted the containment metadata instead, which answered the open formatting element with a fresh list inside it, so the second item and every one after it nested a list deeper. The walk stops where the parsing algorithm's does, at an element in its special category other than address, div and p, so an item inside a heading, a form, a table, a template or a foreign root still nests as a browser nests it, and the item a select keeps for content it cannot hold is none of the input's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…w text Closing the item for a list item start tag queues the formatting open inside it, and the probe found that formatting reopened inside a textarea or a style that followed, where its tags come out as that element's text. A browser reconstructs formatting around such an element, and again for the text after it, which is what the balancer now does; an element forwarded under a name this receiver does not recognize, an SVG-cased textArea, is asked for separately, since it has no entry on the stack. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tput The review measured a 252x output amplification on a shipped policy: one list item after another closing over the same open formatting element queued one more copy each time. The queue now keeps at most three of a name, as a browser's list of active formatting elements does through its Noah's Ark clause, which is the bound a browser has; the same input is now five times its length, not two hundred and fifty. An element with no output bounds nothing a browser reading the output can see, so it no longer stops the walk for the item to close, which the review found made a first pass keep an item the second pass closed. A name the specification calls special but Chrome does not, dialog and search, is no barrier either, as this receiver's input tracker and testDialogElementCategoryIsNotReliedOn already say. Preparation no longer re-runs the walk when it re-enters itself, since the barriers it honoured may have closed since, which made a select decide the item's fate through the close loop rather than the walk. Formatting is reconstructed only where it needs no wrapper implied under the container, so a style after the items is the list's own child, as a browser has it, and the output is a fixed point. It is not reconstructed around an element whose content a browser reads as text either, only for the text that follows it, and an element forwarded inside such a one at any depth counts, not only the innermost. The check is asked once per preparation rather than once per resumption. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ption's item A table this receiver closed in the output to put content in front of it, and an entry whose output is a foreign element, are judged as the output shows them, not as the logical stack holds them, so the first pass no longer keeps an item that the second pass closes. An option stays a barrier although a browser walks past one: a list this receiver opens inside an option holds the item it writes for the list's content, and closing that item for the next start tag would unwrite on the next pass what this pass wrote. The probe is down from 52 flagged rows on 43 inputs to 22 on 16; the remaining ones are listed in the pull request and this change is not ready to merge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jmanico
marked this pull request as draft
September 19, 2026 16:06
…hat changed An element the renderer writes as a pre, an xmp, listing or plaintext, has no raw text in the output, so formatting is reconstructed for it as for any other element; xmp is also the one raw-text start tag whose own rule reconstructs it. The barrier for the walk is the emitted element, not the name the input wrote, so a policy that renames a barrier to something else leaves no barrier for the next pass to disagree about. The resumption queue drops the earliest of four alike, as the Noah's Ark clause says, not the innermost. The change log describes what this now does, including the queue's bound and where formatting is reconstructed, and the stranded doc comment is back on the method it describes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # change_log.md # owasp-java-html-sanitizer/src/test/java/org/owasp/html/HtmlSanitizerTest.java
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 8 of #492: a list-item start after reconstructed formatting could nest
inside the preceding item instead of closing it and becoming its sibling.
What broke
Sanitizers.BLOCKS.and(Sanitizers.FORMATTING)on<math><b>x</math><li>y<li>zproduced nested list items. The same happenedfor ordinary lists such as
<ul><li><b>x<li>y: containment metadata saw theopen formatting element and supplied a fresh list inside it, while a browser's
list-item start algorithm first closes the open
lithrough that formattingand reconstructs the formatting in the sibling item.
What changed
TagBalancingHtmlStreamEventReceiverapplies the list-item start walk tothe emitted HTML context, then uses the existing close-and-reconstruct path
so formatting resumes inside the sibling item.
select/option/optgroupboundaries when thepolicy drops their output, and does not cross a forwarded foreign root in a
way that would reinterpret foreign PCDATA as HTML raw text.
browser would insert that element beside the formatting. The queue retains
the browser's three-of-a-name Noah's Ark bound.
seam so a dropped or renamed wrapper cannot make first and second passes
take different paths.
be emitted and accounts for the physical output budget of a sibling item,
including later dynamic limit changes.
of unknown elements followed by list items linear.
li/option, fixed-limit, anddynamic-limit regressions are covered.
change_log.mddocuments thebehavior change.
The branch was updated by merging current
mainataca9a0ein11bda2d;the stabilization commit is
7ae2b5c.Verification
dropped
select/option/optgroup,li-only/li-option/li-select,forwarded SVG raw-text payloads, and the two nesting-limit residuals.
mainataca9a0e, using the identicalprobe source, compiled probe, corpus, policies, and seeds:
f56c346716f848914be899f37b1b422c1a84b113a97526b71691ca76e33ba9725ae0cbd95686e8d5130f3b2dce2c8e57a6f9ebfffde5bb2a7936c83b693792b18b652c439d4c5584dbded092b21a4c01866f2fc66d631d30413fd634788ee083flags; 903 flags removed (727
non-idempotent, 173tree-mutates,3
fidelity); 0 metadata, row, or total mismatches.0 lost flags; 0 metadata, row, or total mismatches.
./mvnw -o -ntp -B clean verify: success; 697 library tests and 7 exampletests, 0 failures or errors, plus the JPMS consumer verification.
Part of #492 (item 8)