You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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 > textarea — li 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.
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.
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 A form directly inside a table stays open as a container #484 bug.
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.
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.
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 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.
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.
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.
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.
Five balancer behaviours found while reviewing #490 that are present on
main(569d463) todayand 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 policyallowing
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 isul > {SVG}svg > li > textarea—liis an HTML breakout name, so thetextareabecomes anHTML
textarearather than the SVG element the input had. Same with<a>and<mi>. Thecontainment metadata has no entry for foreign content, and text inside a foreign element under
a
ulstill gets thelithe metadata implies forul. Skipping HTML containment for foreigntext is not safe as things stand: the policy's text gate treats a foreign
tbodyas an HTMLone and would drop
Din<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 thecaptionelement 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 tablethere and keeps the text inside the form, which is the A form directly inside a table stays open as a container #484 bug.
openDocument()does not reset the stacks; onlycloseDocument()does. A caller whosereceiver throws mid-document never reaches
closeDocument(); the nextopenDocument()thenstarts with the previous document's
openElementsand the fourteen parallel structures, andthe second document's output begins with the first document's close tags:
</tr></tbody></table><svg><path>y</path></svg>.PolicyFactorybuilds a fresh balancer percall 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) growsthe 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 thefive above, items 6 and 7 are not on
main, which keeps the text; they were accepted for themerge because each needs several unusual things at once and none is an XSS. Item 8 has a
sibling on
main. Item 9 is onmaintoo. Reproductions and the probe are in the localprobes-review3folder 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 wordtailis gone (main:<svg><tbody><table><foreignObject><form>tail</form>…). A browserpops the
svgwhen thetablebreaks out, but the sanitizer writes the table inside thesvg, and the policy's rule for text in a suppressed table part fails closed once theoutput 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 drops
table, 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 (mainkeepsboth). 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 sideby 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 fixedpoint. Policy drops the table parts:
<table><th><option>tail→<option>tail</option>,and the next pass gives
<select><option>tail</option></select>. Onmain,33c2b90andthe fixed head alike. On the fixed head this surfaces in about twenty random probe inputs
that a queued
nobr/bused to mask by being resumed around the option, with the selectimplied inside it.
From the third review comment and the fix-series comment on #490.