OWASP / OWASP/java-html-sanitizer

Tag balancer: five foster-parenting and text-gate gaps present on main

Open
#492 1 comment 0 reactions 0 assignees View on GitHub

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.

  1. An implied <li> lands inside a foreign root and flips a child's namespace. With a policy
    allowing ul, 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 > textareali is an HTML breakout name, so the textarea becomes an
    HTML textarea rather 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
    a ul still gets the li the metadata implies for ul. Skipping HTML containment for foreign
    text is not safe as things stand: the policy's text gate treats a foreign tbody as an HTML
    one and would drop D in <ul><svg><tbody>D.

  2. Sanitizers.TABLES emits 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
    caption element entirely, leaving bare text. Same with <svg> for <math> and <colgroup>
    for <caption>. A published prepackaged policy.

  3. disallowTextIn is bypassed for text foster-parented out of a retired table.
    new HtmlPolicyBuilder().allowElements("table","tbody","tr","td","form").disallowTextIn("form")
    on <tbody><form>B emits B (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. main suppresses it only because it never retires the table
    there and keeps the text inside the form, which is the #484 bug.

  4. openDocument() does not reset the stacks; only closeDocument() does. A caller whose
    receiver throws mid-document never reaches closeDocument(); the next openDocument() then
    starts with the previous document's openElements and 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>. PolicyFactory builds a fresh balancer per
    call so it is unaffected; direct users of the public class are not.

  5. A foreign root with content inside a select grows 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.

  1. 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
    tail is gone (main: <svg><tbody><table><foreignObject><form>tail</form>…). A browser
    pops the svg when the table breaks 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.

  2. A second, pointer-ignored form in a row of a dropped table inside an integration point
    loses the text after the table.
    Policy drops table, keeps form/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>; z is gone (main keeps
    both). Without the second form, or outside the integration point, z is kept.

  3. A list item after a resumed formatting element nests inside the previous item.
    <math><b>x</math><li>y<li>z under Sanitizers.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. main does the same for <ul><li><b>x<li>y: the resumed
    <b> hides the open li from the next <li> start, which should close it.

  4. An option under a dropped table cell is emitted without its select and 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>. On main, 33c2b90 and
    the fixed head alike. On the fixed head this surfaces in about twenty random probe inputs
    that a queued nobr/b used 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.