OWASP / OWASP/java-html-sanitizer
Tag balancer: five foster-parenting and text-gate gaps present on main
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 952
- Forks
- 237
- Avg merge
- 1h 29m
- Merged PRs (30d)
- 77
Description
Five balancer behaviours found while reviewing #490 that are present on main (569d463) today
and are not changed by that PR. Each was reproduced on main, the #490 base, and the #490 head;
none is an XSS, and none needs an advisory. They are here so they are not lost.
-
An implied
<li>lands inside a foreign root and flips a child's namespace. With a policy
allowingul,li,svg,textarea:<ul><svg><textarea>x</textarea></svg></ul>→
<ul><svg><li><textarea>x</textarea></li></svg></ul>. The browser's tree of that output is
ul > {SVG}svg > li > textarea—liis an HTML breakout name, so thetextareabecomes an
HTMLtextarearather than the SVG element the input had. Same with<a>and<mi>. The
containment metadata has no entry for foreign content, and text inside a foreign element under
aulstill gets thelithe metadata implies forul. Skipping HTML containment for foreign
text is not safe as things stand: the policy's text gate treats a foreigntbodyas an HTML
one and would dropDin<ul><svg><tbody>D. -
Sanitizers.TABLESemits an orphan table part that is not a fixed point.
<template><ul><math><caption>TEXT→<caption>TEXT</caption>; a second pass gives
<table><caption>TEXT</caption></table>, and a browser parsing the first output drops the
captionelement entirely, leaving bare text. Same with<svg>for<math>and<colgroup>
for<caption>. A published prepackaged policy. -
disallowTextInis bypassed for text foster-parented out of a retired table.
new HtmlPolicyBuilder().allowElements("table","tbody","tr","td","form").disallowTextIn("form")
on<tbody><form>BemitsB(beside the table). The gate follows the nearest kept element,
and when the table's output is retired for the text, the element the author disallowed text in
is no longer the one judged.mainsuppresses it only because it never retires the table
there and keeps the text inside the form, which is the #484 bug. -
openDocument()does not reset the stacks; onlycloseDocument()does. A caller whose
receiver throws mid-document never reachescloseDocument(); the nextopenDocument()then
starts with the previous document'sopenElementsand the fourteen parallel structures, and
the second document's output begins with the first document's close tags:
</tr></tbody></table><svg><path>y</path></svg>.PolicyFactorybuilds a fresh balancer per
call so it is unaffected; direct users of the public class are not. -
A foreign root with content inside a
selectgrows on every pass.
<select><svg>x</svg></select>→ 38, 47, 56, 65 characters over four sanitizations, one
<ul><li>level per pass, until the nesting limit truncates it. Same for<math>and for
<select><table><svg>x</svg>. Repeated sanitization of stored HTML (edit/save cycles) grows
the document without bound.
From the review comments on #490.
Left over from the third review of #490, accepted for merge (2026-09-18)
These are behaviours of the #490 branch after its fix series (eb11921..6c1fb9f). Unlike the
five above, items 6 and 7 are not on main, which keeps the text; they were accepted for the
merge because each needs several unusual things at once and none is an XSS. Item 8 has a
sibling on main. Item 9 is on main too. Reproductions and the probe are in the local
probes-review3 folder referenced from the PR comments.
-
Text after an emitted breakout inside a lexically open foreign root is dropped by a
deliberate fail-closed rule.<svg><tbody><table><foreignObject><form>tail→ the word
tailis gone (main:<svg><tbody><table><foreignObject><form>tail</form>…). A browser
pops thesvgwhen thetablebreaks out, but the sanitizer writes the table inside the
svg, and the policy's rule for text in a suppressed table part fails closed once the
output tracker has no foreign root left. The rule is intentional; the open question is
whether it should apply here, or whether the balancer should close the foreign root when it
emits a breakout. Same class: one random probe input under the table-dropping and
table-renaming policies where a stale<b>used to slip the text past the rule. -
A second, pointer-ignored form in a row of a dropped table inside an integration point
loses the text after the table. Policy dropstable, keepsform/tr/td:
<svg><foreignObject><table><form id=a><tr><form id=r><td>y</td></tr></table>z→
<svg><foreignObject><form id="a">y</form></foreignObject></svg>;zis gone (mainkeeps
both). Without the second form, or outside the integration point,zis kept. -
A list item after a resumed formatting element nests inside the previous item.
<math><b>x</math><li>y<li>zunderSanitizers.BLOCKS.and(FORMATTING)→
<b>x</b><ul><li><b>y<ul><li>z</li></ul></b></li></ul>, a browser puts the two items side
by side, both bold. Stable output.maindoes the same for<ul><li><b>x<li>y: the resumed
<b>hides the openlifrom the next<li>start, which should close it. -
An
optionunder a dropped table cell is emitted without itsselectand is not a fixed
point. Policy drops the table parts:<table><th><option>tail→<option>tail</option>,
and the next pass gives<select><option>tail</option></select>. Onmain,33c2b90and
the fixed head alike. On the fixed head this surfaces in about twenty random probe inputs
that a queuednobr/bused to mask by being resumed around the option, with the select
implied inside it.
From the third review comment and the fix-series comment on #490.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reproducing the five main cases against commit 569d463 and inspect the balancer's containment, text-gate, and document-stack paths, including openDocument() and closeDocument(). Compare the accepted review cases with the referenced probes-review3 folder, then separate the behaviors into focused fixes. Done means the listed namespace, fixed-point, text-filtering, reset, and repeated-sanitization failures no longer reproduce, with regression coverage for each.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- backend, security
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100