Skip to content

feat: count template filters as calls to the functions behind them - #173

Merged
SizzleUnrlsd merged 4 commits into
mainfrom
feat/template-filter-calls
Sep 27, 2026
Merged

SizzleUnrlsd merged 4 commits into
mainfrom
feat/template-filter-calls

Conversation

@SizzleUnrlsd

Copy link
Copy Markdown
Contributor

Part of #146, first and third items. The second item, data flowing from the render context to the filter's argument (the exploitable level), follows in its own PR.

Summary

  • A filter of Django's own libraries that a project template applies is a call to the function behind it, at the template's line. This covers filters in variables, in tag arguments ({% if bio|striptags %}) and in {% filter %} blocks. Comments, {% comment %} and {% verbatim %} blocks, quoted text and custom filters apply none. Every template found under a templates directory counts, whether or not a Python call names it: a class-based view renders its template_name inside Django.
  • taint.templates.FILTER_FUNCTIONS maps the 73 filters to the functions Django registers them from. It includes the renamed ones (escape → escape_filter, slice → slice_filter, timezone → tz.do_timezone) and those of i18n, l10n, tz and humanize. I checked it against the Django 5.2.6 sources by AST: nothing missing and no mismatch. length_is is kept because it exists up to Django 5.0. The escaping judgement now derives its filter names from this table; the set is identical (71 names).
  • ProjectContext.templates (ProjectTemplates: names, calls, unread) gives the plugins the templates found and the filters they apply. reachable-vulnerability reports an entry point that a filter reaches at the template's line.
  • Where the project names a template the engine cannot read, Coverage.unread_templates lists it. That means a name given by an expression, or a name found under no templates directory, in render, render_to_string, TemplateResponse, {% include %} or {% extends %}; an unreadable template file counts too. The VEX report then keeps an advisory whose entry points include a template filter under_investigation, and the notes say where. Advisories that no template can reach are not concerned.
  • django.shortcuts.render, TemplateResponse and SimpleTemplateResponse are now TemplateRender models, like render_to_string.

Limits

  • The data a template passes to a filter is not tracked yet, so these calls are reachable, not exploitable. That is the next PR.
  • A template named elsewhere is not checked for being unread, for example a class-based view's template_name or get_template.
  • Every file under a templates directory is read as a Django template. A Jinja2 template applying striptags, in a project that requires a vulnerable Django, would count as a call.
  • Unchanged here: escaped_templates still reads an unreadable template file as empty text, and so judges it escaping.

Verification

  • RED: before the feature, the new tests had 23 failures, each for the expected reason (no filter call, not_affected).
  • tests/test_template_filter_calls.py: 32 passed. Full suite under Python 3.12.10, regression snapshots included: 1336 passed. ruff check and mypy pass.
  • Mutations, all killed: ignoring an unreadable file (1 failure), applying the VEX rule to every advisory (1 failure), never applying it (6 failures).
  • On the Django 5.2.6 source tree (3,668 files), reading its templates finds 144 filter calls and 6 includes named by an expression, in 0.4 s.

A filter of Django's own libraries that a project template applies runs the
function behind it whenever the template is rendered. The tests specify that
it counts as a reachable call at the template's line, in variables, tag
arguments and filter blocks, and that a template the engine cannot read keeps
an advisory a template filter can reach under investigation in the VEX report.
Project templates are read for the filters they apply, each mapped to the
function Django registers it from, and reachable-vulnerability reports an
entry point a template filter reaches at the template's line, whether or not
a Python call names the template. The escaping judgement reads its filter
names from the same table.

Where the project names a template the engine cannot read (by an expression,
or by a name found under no templates directory, in a render call, an include
or an extends), coverage lists it, and the VEX report keeps an advisory whose
entry points include a template filter under investigation. render,
TemplateResponse and SimpleTemplateResponse are template renders like
render_to_string.
@SizzleUnrlsd SizzleUnrlsd added the enhancement New feature or request label Sep 26, 2026
@SizzleUnrlsd SizzleUnrlsd self-assigned this Sep 26, 2026
{% filter striptags %} has no | before its filter; it is the usual form of a filter block.
A {% filter %} tag names its first filter without a | before it, so {% filter striptags %} applied no filter. The tag's argument is now read as a chain of filters.
@SizzleUnrlsd

Copy link
Copy Markdown
Contributor Author

Fix pushed (421046c, 3174c9a): {% filter striptags %} applied no filter, because a {% filter %} tag names its first filter without a | before it. The existing test only covered {% filter lower|striptags %}, where striptags follows a |. A new case covers the single-filter form. On the Django 5.2.6 source tree, admin/index.html:41 ({% filter capfirst %}) is now counted: 145 filter calls instead of 144. Full suite under Python 3.12.10: 1337 passed; ruff and mypy pass.

@SizzleUnrlsd
SizzleUnrlsd merged commit 8e128be into main Sep 27, 2026
10 checks passed
@SizzleUnrlsd
SizzleUnrlsd deleted the feat/template-filter-calls branch September 27, 2026 14:43
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.

1 participant