From 3f8e1c66702d77dc7b949fee8a5028d4fc742a51 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 23 Oct 2026 20:41:46 +0200 Subject: [PATCH 1/3] fix(templates): count a comment holding template tags left open as not ending in HTML data The reading ended a comment holding template tags at its first tag. Those tags may also render nothing, and the comment, left open, then hides what follows. A text ending in such a comment therefore does not end in HTML data, and an include of a name several files have, one of which ends so, is refused (#273). --- src/coretrace_python/taint/templates.py | 8 +- tests/test_template_comment_continuations.py | 81 ++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 tests/test_template_comment_continuations.py diff --git a/src/coretrace_python/taint/templates.py b/src/coretrace_python/taint/templates.py index e18312b..123df62 100644 --- a/src/coretrace_python/taint/templates.py +++ b/src/coretrace_python/taint/templates.py @@ -773,7 +773,8 @@ def _read_markup(html: str) -> _Reading: include there may close the comment, a condition around its ``<{{ name }}">, the comment runs to the "-->" in the title, and the variable after it names a tag (#278). The reading resumed after the first tag is kept, and it must be in HTML data right after each later tag of the comment and right after its close, where the other readings resume; where it is not, the text is refused. An inserted markup anywhere inside such a comment is refused. The reading stays linear. The points of resumption come in two sequences that never go back, each held in a queue, and inserted markups are looked up by position. On 1.7 million generated texts, it never accepts a text that a reading of every continuation refuses. --- src/coretrace_python/taint/templates.py | 72 ++++++++-- tests/test_template_comment_continuations.py | 140 +++++++++++++++++++ 2 files changed, 202 insertions(+), 10 deletions(-) diff --git a/src/coretrace_python/taint/templates.py b/src/coretrace_python/taint/templates.py index 123df62..01a30bc 100644 --- a/src/coretrace_python/taint/templates.py +++ b/src/coretrace_python/taint/templates.py @@ -49,6 +49,8 @@ from __future__ import annotations import re +from bisect import bisect_left +from collections import deque from collections.abc import Container, Iterable, Iterator, Mapping, Sequence from dataclasses import dataclass, field from pathlib import Path @@ -773,13 +775,25 @@ def _read_markup(html: str) -> _Reading: include there may close the comment, a condition around its ``" in the title, and the variable after it names a tag. +def test_a_comment_closed_inside_an_attribute_after_its_tags_is_cross_site_scripting(tmp_path: Path) -> None: + templates = {"page.html": '<{{ name }}">x\n'} + + assert xss(tmp_path, templates) == [("app/views.py", 5)] + + +@pytest.mark.parametrize( + "template", + [ + '<{{ name }}">x\n', + '\n', + ], + ids=["close-inside-a-title", "later-tag-inside-a-title"], +) +def test_a_comment_resuming_elsewhere_than_in_html_data_is_not_established(tmp_path: Path, template: str) -> None: + assert escaped(tmp_path, {"page.html": template}) == [] + + +@pytest.mark.parametrize( + "template", + [ + "\n", + "

{{ name }}

\n", + '\n', + '

hello

\n', + ], + ids=["tags-around-text", "variable-after-the-comment", "tags-in-a-class", "no-tag"], +) +def test_a_comment_resuming_in_html_data_everywhere_stays_established(tmp_path: Path, template: str) -> None: + assert escaped(tmp_path, {"page.html": template}) == ["page.html"] + + +def test_an_include_of_a_name_two_files_have_anywhere_in_a_comment_is_refused(tmp_path: Path) -> None: + # The tag before it may render nothing: the include then stands inside the comment. + templates = { + "part.html": "x\n", + "other/templates/part.html": "y\n", + "page.html": "\n", + } + + assert escaped(tmp_path, templates) == ["part.html"] + + def test_a_comment_holding_tags_left_open_does_not_end_in_html_data(tmp_path: Path) -> None: # Its tags may render nothing: the comment then hides what follows the include. templates = { @@ -79,3 +130,92 @@ def test_a_comment_holding_tags_left_open_does_not_end_in_html_data(tmp_path: Pa } assert escaped(tmp_path, templates) == ["part.html"] + + +@pytest.mark.parametrize( + "shape", + [ + "tags-in-one-open-comment", + "tags-in-one-closed-comment", + "tag-then-open-start-tag", + "tag-then-open-script", + "comments-with-tags-in-titles", + ], +) +def test_the_points_of_resumption_keep_the_reading_linear(tmp_path: Path, shape: str) -> None: + template = { + "tags-in-one-open-comment": "", + "tag-then-open-start-tag": "' * (SIZE // 38), + }[shape] + + escaped(tmp_path, {"page.html": template}) + + +# Pieces of a template's markup as the reader sees it: markup, a variable, a template tag +# and an inserted markup (#273). +OUTPUT, TAG, INSERTED = "\x00", "\x01", "\x02" +PIECES = ["", "", "--!>", TAG, OUTPUT, '", "", "x", " ", "<", " bool: + """The reading of ``html`` that follows every continuation of each comment holding + template tags: from each of its tags, and from its close. Its cost is not linear; + it is the reference the reading of the engine must never be more lenient than.""" + + from coretrace_python.taint import templates + + @cache + def from_position(position: int) -> bool: + while (start := html.find("<", position)) != -1: + if html.startswith("