diff --git a/docs/usage.md b/docs/usage.md index e43f262..a22a5f6 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -197,10 +197,17 @@ It establishes escaping only when all of the following hold: or an attribute name. - No Python file of the project sets `autoescape` to `False`. -Start tags and their attributes are read as a browser's HTML tokenizer reads them, in time -linear in the size of the template. A tag, or a quoted attribute value, a template leaves -open at its end is checked too: once the template is included, it runs into the markup -that follows, the value staying quoted until its quote closes. +Templates are read as a browser's HTML tokenizer reads them, in time linear in the size +of the template. What a template leaves open at its end is checked too, since once the +template is included it runs into the markup that follows: a tag's attributes are +checked, a quoted value stays quoted until its quote closes, and a variable in a +`\n"}, + {"page.html": '\n', "close.html": "-->"}, + ], + ids=[ + "A-fragment-closed-by-includer", + "B-closed-in-same-template", + "C-open-to-end-of-document", + "D-comment-closed-by-include", + "E-includer-continues-open-script", + "G-closed-comment-holding-include", + ], +) +def test_a_variable_the_rendered_page_runs_as_script_is_cross_site_scripting( + tmp_path: Path, templates: dict[str, str] +) -> None: + assert xss(tmp_path, templates) == [("app/views.py", 5)] + + +def test_a_variable_in_a_comment_holding_no_template_tag_stays_escaped(tmp_path: Path) -> None: + templates = {"page.html": "

hello

\n"} + + assert escaped(tmp_path, templates) == ["page.html"] + assert xss(tmp_path, templates) == [] + + +@pytest.mark.parametrize( + "template", + [ + '{% if debug %}', + '', + '', + '', + ], + ids=["condition-around-the-opener", "include-inside", "abrupt-empty", "abrupt-dash", "bang-close"], +) +def test_a_comment_ends_at_its_close_or_at_a_template_tag_inside_it(tmp_path: Path, template: str) -> None: + assert escaped(tmp_path, {"page.html": template, "x.html": ""}) == ["x.html"] + + +@pytest.mark.parametrize( + "template", + ['-->', ' -->'], + ids=["abrupt-before-a-later-close", "bang-before-a-later-close"], +) +def test_a_comment_closes_where_a_browser_closes_it(tmp_path: Path, template: str) -> None: + # Read to the later ``-->``, the handler was taken for part of the comment. + assert escaped(tmp_path, {"page.html": template}) == [] + + +def test_a_comment_left_open_without_a_template_tag_is_inert_to_the_end(tmp_path: Path) -> None: + # Rendered alone, the comment runs to the end; included, it holds the markup that + # follows until a ``-->`` there: either way, nothing after its ``\n

{{ name }}

\n"} + + assert escaped(tmp_path, templates) == ["fragment.html", "page.html"] + + +@pytest.mark.parametrize( + "chain", + [ + {"child.html": '{% extends "base.html" %}{% block body %}hello{% endblock %}'}, + { + "middle.html": '{% extends "base.html" %}', + "child.html": '{% extends "middle.html" %}{% block body %}hello{% endblock %}', + }, + ], + ids=["extends", "extends-twice"], +) +def test_a_template_extending_one_that_ends_inside_a_construct_ends_inside_it_too( + tmp_path: Path, chain: dict[str, str] +) -> None: + # Rendered, child.html is base.html with its block filled: it ends inside base.html's + # script, which the markup after its include continues. + templates = { + "base.html": "

{% block body %}{% endblock %}

', + } + + assert escaped(tmp_path, templates) == sorted(set(templates) - {"page.html"}) + assert xss(tmp_path, templates) == [("app/views.py", 5)] + + +def test_extending_a_template_that_ends_inside_a_script_stays_established(tmp_path: Path) -> None: + # A child's blocks stand where the parent places them, before the parent's end. + templates = { + "base.html": "

{% block body %}{% endblock %}