Skip to content

fix: clear render_to_string output only for templates shown to escape - #131

Merged
SizzleUnrlsd merged 3 commits into
mainfrom
fix/template-escaping
Sep 24, 2026
Merged

SizzleUnrlsd merged 3 commits into
mainfrom
fix/template-escaping

Conversation

@SizzleUnrlsd

Copy link
Copy Markdown
Contributor

Closes #130. This is the second model fix that comes before the stricter guard rule from #98. It is independent of #129; the two only touch the same CHANGELOG spot, and I'll merge main in to resolve that once one of them lands.

The three pygoat findings, checked

introduction/views.py:290, 304 and 318 were reported as xss on HttpResponse(render_to_string('Lab/AUTH/auth_success.html', {...})).

  • The template. It prints {{ username }}, {{ name }}, {{ userid }} and {{ err_msg }} in HTML text only, with no |safe and no {% autoescape off %}.
  • What it extends. It extends introduction/base.html, which loads only static and prints no context variable.
  • Settings. The backend is DjangoTemplates with default options, so autoescaping is on.
  • Where the output goes. The rendered string goes straight into an HTML HttpResponse.

All three are false positives.

Change

Escaping is modelled only where the engine can establish it.

  • taint.templates.escaped_templates(root) reads the project's templates, found under templates directories by the path Django uses. It returns those that escape everything they print. A template qualifies only if all of these hold:
    • its whole extends/include closure is found;
    • no |safe, |safeseq or {% autoescape off %};
    • no tag, filter or tag library beyond Django's own;
    • no variable in a context HTML escaping does not protect: <script>/<style>, event handlers, style/srcdoc, a URL attribute not fixed by a relative or http(s)://host/ prefix, unquoted attributes, attribute or tag names;
    • every template of that name qualifies;
    • no Python file of the project sets autoescape to False.
  • The TemplateRender(symbol, position=0, keyword="template_name") model declares django.template.loader.render_to_string. When a call writes the template name as a constant and that template is established, its result loses HTML taint. Other kinds are unaffected.
  • The engine computes the set once per directory check. It passes the set to worker processes and includes it in the cache key, so a template that gains |safe invalidates cached results.
  • Documented limitation. No escaping is assumed for a template named by an expression, found through a DIRS entry not named templates, shipped by an installed package, rendered through get_template, or a Flask Jinja2 template. Single-file checks read no templates. The usage guide explains this, and a regression test covers the DIRS case.

Verification

  • pytest: 1028 passed. ruff check src tests and mypy are clean.
  • pytest -m regression under Python 3.12: 19 passed. Only the pygoat snapshot changed, and only by those three xss findings.
  • Scanner on pygoat. 101 of its 120 templates are established. The XSS lab templates that really use {{ ...|safe }} are not; Lab/XSS/xss.html mentions |safe only in prose, as {query|safe} inside <code>, so it is correctly established.
  • Mutation checks. Each of these changes fails at least one test: a missing model, a template name ignored, filters ignored, script/style ignored, the URL prefix ignored, no extends/include closure, any candidate instead of every one, settings ignored, templates left out of the cache key, and workers not receiving the set.
  • Cost. The scan costs 0.56 s on healthchecks (136 of its templates established), against about 13 s for the analysis itself.
  • Architecture. test_architecture forbids parsers outside the frontend, so template names are compared as call sites record constants (repr) rather than parsed.

# Conflicts:
#	CHANGELOG.md
#	docs/usage.md
@SizzleUnrlsd
SizzleUnrlsd merged commit a249d7a into main Sep 24, 2026
10 checks passed
@SizzleUnrlsd
SizzleUnrlsd deleted the fix/template-escaping branch September 24, 2026 22:09
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.

Precision: establish Django template autoescaping before clearing render_to_string output

1 participant