Skip to content

feat: carry render context data to the template filters it reaches - #174

Merged
SizzleUnrlsd merged 5 commits into
mainfrom
feat/template-context-flows
Sep 27, 2026
Merged

SizzleUnrlsd merged 5 commits into
mainfrom
feat/template-context-flows

Conversation

@SizzleUnrlsd

Copy link
Copy Markdown
Contributor

Closes #146. This covers the second item: attacker input in the render context reaching a filter makes the entry point exploitable, not only reachable. The first and third items came with #173.

Summary

  • Render context. A filter is exploitable at the render call only where the engine is certain of the value that reaches it. The context must be a dict literal with constant keys that the render call alone uses, whether written in the call or passed through a variable. A mutated dict, one passed elsewhere, **extra, dict(...), or any other context leaves the filter reachable.

  • Template scopes. A filter's value is followed from a context variable directly, or through {% for %}, {% with %}, the as of other tags, constant {% include %} (with its with bindings) and {% extends %}, the arguments of earlier filters, and the output of {% filter %} blocks. A name a tag binds shadows the context. {% include ... only %} passes none of it.

  • Blocks. A template extending another renders only its blocks, which replace the blocks of that name in the parent. Overridden parent blocks and content outside the child's blocks are therefore not counted.

  • Duplicate names. A template name found in several files gives no flows, since the loaders decide which one renders.

  • Filter arguments. A filter's value is its first argument and the filter's own argument (cut:bio) its second. attacker_arguments restricted to the value is therefore not exploitable through the argument.

  • Project functions. A function rendering the values it receives carries them to the filters: function summaries record the filter calls.

  • Request context processor. {{ request.GET.q|striptags }} counts only when all of these hold:

    • every DjangoTemplates engine of TEMPLATES lists django.template.context_processors.request in literals, checked on the HIR, and no other code names TEMPLATES;
    • the render passes the request (render, TemplateResponse, render_to_string given request);
    • the context is certain (absent or a literal), and nothing shadows request;
    • the template reads a text attribute of the request object model (GET, POST, COOKIES, headers…), not user or session.

    Through a project function, only the context entries are followed.

  • Engine wiring.

    • TemplateCalls (interprocedural layer) is provided to every module, including those analysed in other processes, and is part of the cache key.
    • TemplateRender gains context and request positions.
    • The template reader now parses Django's filter-expression grammar instead of scanning for |name.

Limits

  • A child block nested inside a parent block that the child overrides without re-including it may still be counted.
  • {{ block.super }} is not followed.
  • Anything the engine cannot prove stays reachable.

Verification

  • RED before each feature commit. test_template_context_flows.py: 22, then 3 failures. test_template_request_processor.py: 12 failures. Each failed for the expected reason: no exploitable finding.
  • Full suite under Python 3.12.10, regression snapshots included: 1410 passed. ruff check and mypy pass.
  • 19 mutations of the guards (single use, **, duplicate files, only, scope ends, block overrides, argument position, summaries, cache key, parallel batch, TEMPLATES proof, processor flag, user text, attribute through bindings, absent context) are all killed. Two surviving mutants showed redundant guards, which were removed.

A context value carrying attacker input reaches the filter a template applies when the context is a dict literal with constant keys that the render call alone uses: through for, with, the templates it includes or extends, earlier filter arguments and filter blocks, and through a project function. Anything else leaves the filter reachable.
A filter in a block the extending template overrides, or outside the blocks of an extending template, does not render, so the context reaching it does not make it exploitable.
A render call whose context is a dict literal with constant keys, used by
that call alone, makes the filter calls the rendered template applies, each
fed from the context entries reaching its value and its argument. The
templates are read for their scopes: for, with and the as of other tags bind
names until their block ends, constant includes map names through their
bindings or pass none with only, and a template extending another renders
only its blocks, which replace the blocks of that name. A name found in
several files gives no filter calls.

The taint engine reports flows into those filters at the render call, naming
the template, and function summaries record them, so a project function
rendering what it receives carries it to the filters. The template calls are
provided to every module, including those analysed in other processes, and
are part of the cache key.
With django.template.context_processors.request certainly enabled in every Django engine the settings configure, a render passing the request with a certain context gives the template the request, whose user-controlled text attributes then reach its filters. Anything uncertain leaves the filter reachable.
When every Django template engine the settings assign to TEMPLATES, as
literals no other code names, lists django.template.context_processors.request,
a render call passing the request with a certain context gives the template
the request. Template variables now keep their first attribute, so a filter
reading request.GET, directly or through for, with and includes, is fed from
the text attributes the request object model lists; user, session and
anything shadowed by a context entry, a tag binding or include only are not.
Through a project function, only the context entries are followed.
@SizzleUnrlsd SizzleUnrlsd added the enhancement New feature or request label Sep 27, 2026
@SizzleUnrlsd SizzleUnrlsd self-assigned this Sep 27, 2026
@SizzleUnrlsd
SizzleUnrlsd merged commit 5c00508 into main Sep 27, 2026
10 checks passed
@SizzleUnrlsd
SizzleUnrlsd deleted the feat/template-context-flows branch September 27, 2026 15:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Advisories: template filters as entry points of the functions behind them

1 participant