From 24c6c780837d9cae4d7f872669813bb562de6807 Mon Sep 17 00:00:00 2001 From: Jim Manico Date: Sat, 19 Sep 2026 04:52:16 -0700 Subject: [PATCH 1/3] Give a caption or column group under a dropped template its table in 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 --- change_log.md | 5 ++ .../TagBalancingHtmlStreamEventReceiver.java | 19 +++-- .../org/owasp/html/HtmlSanitizerTest.java | 69 +++++++++++++++++++ 3 files changed, 86 insertions(+), 7 deletions(-) diff --git a/change_log.md b/change_log.md index e7d2581c..453f08e7 100644 --- a/change_log.md +++ b/change_log.md @@ -2,6 +2,11 @@ Most recent at top. * Next release + * A caption or column group under a `template` the policy dropped is + judged in the output, where the template was, and gets its table there, + instead of coming out as an orphan part that a browser drops and the + next pass wraps; `Sanitizers.TABLES` now emits on the first pass what + it emitted on the second (#492, item 2). * An `option` or `optgroup` under any element but a `select`, `optgroup` or `option` now gets its `select`, and a list item under any element but a list its list, whatever the container's name (a list the policy diff --git a/owasp-java-html-sanitizer/src/main/java/org/owasp/html/TagBalancingHtmlStreamEventReceiver.java b/owasp-java-html-sanitizer/src/main/java/org/owasp/html/TagBalancingHtmlStreamEventReceiver.java index 62715b38..984f5b39 100644 --- a/owasp-java-html-sanitizer/src/main/java/org/owasp/html/TagBalancingHtmlStreamEventReceiver.java +++ b/owasp-java-html-sanitizer/src/main/java/org/owasp/html/TagBalancingHtmlStreamEventReceiver.java @@ -3097,19 +3097,24 @@ && hasUnavailableOutputlessTable()) { && outputElements.get(containerIndexOnStack) != TEMPLATE_TAG) { return BODY_TAG; } - if ((child == OPTION_TAG || child == OPTGROUP_TAG) + if ((child == OPTION_TAG || child == OPTGROUP_TAG + || child == CAPTION_TAG || child == COLGROUP_TAG) && !isOutputInForeignContent() && containerIndexOnStack >= 0 && containerIndexOnStack < openElements.size() && openElements.get(containerIndexOnStack) == TEMPLATE_TAG && outputElements.get(containerIndexOnStack) != TEMPLATE_TAG && sentToUnderlying.get(containerIndexOnStack)) { - // A template holds an option or optgroup directly, so the containment - // metadata implies no select for one there, but a template the policy - // dropped or renamed establishes none in the output: the option lands - // where the template was, or in what the template became, and is - // judged there, so it gets the select it gets in that place instead - // of coming out bare for the next pass to wrap (#492). + // A template holds an option, optgroup, caption or column group + // directly, so the containment metadata implies no select or table + // for one there, but a template the policy dropped or renamed + // establishes none in the output: the element lands where the + // template was, or in what the template became, and is judged there, + // so it gets the select or table it gets in that place instead of + // coming out bare, an orphan caption that the output parser drops, + // for the next pass to wrap (#492, items 9 and 2). The other table + // parts already get their table under a template through the + // metadata. int outputContainer = outputContainerIndex(); return outputContainer != UNRECOGNIZED_TAG ? outputContainer : BODY_TAG; } diff --git a/owasp-java-html-sanitizer/src/test/java/org/owasp/html/HtmlSanitizerTest.java b/owasp-java-html-sanitizer/src/test/java/org/owasp/html/HtmlSanitizerTest.java index 8e384ff1..5a64c683 100644 --- a/owasp-java-html-sanitizer/src/test/java/org/owasp/html/HtmlSanitizerTest.java +++ b/owasp-java-html-sanitizer/src/test/java/org/owasp/html/HtmlSanitizerTest.java @@ -5784,6 +5784,75 @@ public String apply(String elementName, List attrs) { ""); } + /** + * Item 2 of #492. A template holds a caption or column group directly, + * so the containment metadata implies no table for one there. A template + * the policy dropped establishes no template in the output, though: the + * caption landed where the template was, as an orphan that a browser + * drops, and the next pass gave it its table. Parts under a dropped + * template are now judged where the template was. The prepackaged + * policies drop the template and, being allowed no `table` they did not + * emit before, the output narrows to what the second pass already gave. + */ + @Test + void testTablePartUnderDroppedTemplateGetsItsTable() throws Exception { + String[][] tables = { + { "