Skip to content

Give a caption or column group under a dropped template its table in Sanitizers.TABLES - #499

Merged
jmanico merged 3 commits into
mainfrom
tables-caption-under-dropped-template-492-2
Sep 19, 2026
Merged

jmanico merged 3 commits into
mainfrom
tables-caption-under-dropped-template-492-2

Conversation

@jmanico

@jmanico jmanico commented Sep 19, 2026

Copy link
Copy Markdown
Member

Item 2 of #492: Sanitizers.TABLES emits an orphan table part that is not a fixed point.

What broke

Sanitizers.TABLES.sanitize("<template><ul><math><caption>TEXT") gave <caption>TEXT</caption>, a caption with no table, which a browser drops; the next pass gave <table><caption>TEXT</caption></table>. Same for <template><caption>TEXT, for a colgroup, for a col, and under any policy that drops or renames template: <u><template><caption>x gave <caption>x</caption>.

Why

A template holds a caption, column group or col directly, so the containment metadata implies no table for one there, and the balancer judged the part against its logical container, the template, though the policy had dropped it. A dropped template establishes nothing in the output: the part lands where the template stood, as an orphan. The other table parts already got their table under a template through the metadata, which is why a td there was a fixed point.

What changed

  • TagBalancingHtmlStreamEventReceiver.effectiveContainer: the rule that Give a free option or list item its wrapper under every container, and push the select out of a kept table #497 added for an option or optgroup under a template the policy dropped or renamed, which judges the element against the output's container instead of the template, now covers caption, colgroup and col. Those five are exactly the elements a template holds directly and body does not.
  • The same method's caller for an orphan part: a dropped template is not a dropped raw-text container, so its content is not suppressed. Without that, a policy with disallowTextIn("template") lost the text of the caption it keeps.
  • Tests in HtmlSanitizerTest; change_log.md bullet.

Before and after

Input Policy main, passes 1 and 2 This branch
<template><ul><math><caption>TEXT Sanitizers.TABLES <caption>TEXT</caption>, then with its table <table><caption>TEXT</caption></table>
<template><ul><math><colgroup>TEXT same <colgroup></colgroup>TEXT, then with its table <table><colgroup></colgroup></table>TEXT
<template><col>x same <col />x, then with its table <table><colgroup><col /></colgroup></table>x
<template><caption>TEXT</caption></template>after same <caption>TEXT</caption>after, then with its table <table><caption>TEXT</caption></table>after
<u><template><caption>TEXT same <caption>TEXT</caption>, then with its table <table><caption>TEXT</caption></table>
<template><caption>TEXT</caption></template> disallowTextIn("template") <caption>TEXT</caption>, then with its table <table><caption>TEXT</caption></table>
<template><ul><math><caption>TEXT BLOCKS.and(TABLES) <ul><li></li></ul><caption>TEXT</caption>, then with its table <ul><li></li></ul><table><caption>TEXT</caption></table>
<template><caption>TEXT</caption></template> template, table, caption allowed unchanged input unchanged input

The output narrows: Sanitizers.TABLES emits on the first pass what it emitted on the second, and allows nothing it did not allow. A policy that drops the caption itself, or disallows text in the template, gets what it gets for the same part written without the template.

Verification

  • ./mvnw -o -ntp -B clean verify: build success, 687 tests, 0 failures (686 existing, unchanged, plus 1 new).
  • Differential probe (probes-review3/ReviewProbe, 15,430 inputs x 44 policies, san and events modes, main 2bc430e against the final commit 9ebd255): san 678,920 rows compared, 330 differ in output, 501 rows lose every flag, 4 keep fewer, no row throws, and 8 rows on 2 inputs gain one. Both are the same shape under the three policies that allow li but no table: a col under a dropped template inside a list gets a table the policy drops, which closes the list item, and the next pass gives the text after it a new item, as <ol><option><col><dl>tail does on main without any template. events: 36,063 rows, identical.

Review

/code-review ran twice. The first pass found the container rule leaving a second table where the output container was an open column group, an orphan caption in an open cell, col left out although the metadata treats it exactly as caption and colgroup, and a change-log bullet that did not mention custom policies. col and the bullet are fixed here.

For the rest, the first pass named a root cause, that a dropped template still bounds table scope, and a version of this branch acted on it in returnToTableContext and the container-closing loop. The second pass reproduced three regressions in exactly those two changes: a part under a dropped template at an HTML integration point came back as the orphan this change exists to remove; the closing loop that returns a part to an open table is the one pop site that never queues formatting to reconstruct, so a u around the caption was destroyed; and judging the container by the output there made a dropped template look like a dropped raw-text container, deleting the caption's own text. Both changes are reverted and the text deletion is fixed; the second pass called the container rule itself "sound and well covered".

Left out

  • A part under a template inside an open table, <table><tr><td><template><caption>x, keeps the shape it has on main: the template still bounds table scope, so the part stays in the cell and the next pass moves it. That is not a fixed point here or on main. Closing that scope belongs with the five other scans that read it the same way, which is its own change.
  • <u><template><caption>x under FORMATTING.and(TABLES) likewise keeps main's shape, non-idempotent there as on main.
  • The five element names are still written out rather than derived from the metadata, as #497 left them.

Part of #492 (item 2)

🤖 Generated with Claude Code

jmanico and others added 3 commits September 19, 2026 04:52
…Sanitizers.TABLES

A template holds a caption or column group directly, so the containment
metadata implies no table for one there, but a template the policy
dropped or renamed establishes none in the output: the part landed where
the template was as an orphan that a browser drops, and the next pass
gave it its table.  The part is now judged in the output, where the
template stood, by the rule that already judges an option there, so
Sanitizers.TABLES emits on the first pass what it emitted on the second.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…a col directly too

A part under a template the policy dropped, inside an open table, now
returns to that table as a browser reading the output does, closing a
column group, cell or row, instead of opening a second table or coming
out bare in the cell: the dropped template is no table-scope boundary in
the output, and the close loop judges what a dropped template can hold
where its content lands, so a caption under one inside a kept formatting
element closes it and gets its table beside it, as without the template.
A col is held directly by a template as well and gets its table the same
way.  Tests cover the open-table, cell, formatting and col shapes, a
hostile column group, and a policy that drops the part itself.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A review reproduced three regressions in the scope and closing changes
this branch added, all in states its tests never entered: a part under a
dropped template at an HTML integration point came out as the orphan this
change exists to remove; the closing loop that returns a part to an open
table is the one pop site that never queues formatting to reconstruct, so
a u around the caption was destroyed; and judging the container by the
output made a dropped template look like a dropped raw-text container, so
the caption's own text was deleted under disallowTextIn("template").

Both changes are reverted.  What stays is the container rule, which the
review found sound: a caption, column group or col under a template the
policy dropped or renamed is judged where the template stood.  The text
deletion is fixed by keeping a dropped template out of the dropped
raw-text container path.  A part under a template inside an open table
keeps the shape it had, which is not a fixed point here or on main; that
belongs with the five other scans that read table scope the same way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jmanico
jmanico merged commit 6d57ddc into main Sep 19, 2026
7 checks passed
@jmanico
jmanico deleted the tables-caption-under-dropped-template-492-2 branch September 19, 2026 16:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant