Skip to content

fix: respect project template filter overrides before reporting built-ins - #189

Merged
SizzleUnrlsd merged 12 commits into
mainfrom
fix/template-filter-overrides
Sep 29, 2026
Merged

SizzleUnrlsd merged 12 commits into
mainfrom
fix/template-filter-overrides

Conversation

@SizzleUnrlsd

Copy link
Copy Markdown
Contributor

Closes #183.

A project tag library registering a filter of a built-in's name replaces the built-in in every template loading it before applying the filter, as Django's parser does. {% load custom %}{{ bio|striptags }} was reported as a reachable, and with attacker input an exploitable, call to django.template.defaultfilters.striptags even when app/templatetags/custom.py registers its own striptags: the project's function is ordinary code, analysed as any other, and the template's call is no evidence about the built-in.

  • _read_scopes resolves each filter where it is applied: the built-in, unless the latest applicable {% load %} so far names a project library that certainly registers the name (nothing recorded) or may (the load is kept as unresolved). Loads are ordered as Django parses them, {% load a from lib %} selects, Django's own libraries change nothing.
  • Libraries are the templatetags/*.py modules discover_files already lists, read with the HIR and the semantic layers: register must resolve to django.template.Library whatever the import spelling; the decorator and call forms of register.filter are read, at module level or under if, try or a function; tag registrars are ignored. Anything the engine cannot follow, a name not written as a constant, unpacked arguments, register passed elsewhere (to a call or a decorator), filters= or register.filters, another Library attribute, a file it cannot read or parse, makes the whole library uncertain, fail-closed.
  • Uncertainty flows through the existing unread channel, app/templates/app/profile.html:1 loads 'custom', not found | not read | found in several files, so Coverage.unread_templates and the VEX under_investigation status need no change. OPTIONS["libraries"] and OPTIONS["builtins"] are not read, and a library found under no templatetags directory, such as one an installed package ships, is uncertain too (documented).
  • escaped_templates is untouched: a template loading an overriding library is still not established to escape.

Tests: tests/test_template_filter_overrides.py (60 cases, 52 failing before the change: reproduction, 17 registration forms, 7 import spellings, load order, selective loads, 11 unreadable forms, two apps agreeing or not, an override calling strip_tags reported at the library's line only, calls/filters consequences); pytest -m "not regression" 1483 passed, 2 skipped; regression corpus 19 passed, snapshots unchanged; ruff check . and mypy clean.

…ocessor

A TEMPLATES bound in a function, called or not, or in a class configures nothing; a function reading, mutating or rebinding the module's TEMPLATES leaves the activation uncertain; a local TEMPLATES beside module settings changes nothing.
request_processor walked every node of every module and counted any assignment to TEMPLATES, so a function-local or class-level assignment activated the request context processor. It now walks the modules with their scope tables: an assignment counts only at module scope, and a mention counts only when the name resolves to the module's binding, or to nothing, as a star import gives. ScopeTable.scope_for accepts a lambda, whose scope the builder already records.
…e flow

A block rendering {{ block.super }} extends the parent's block: its filters stay fed by the render context, with the parent template as the place of the call, however block.super is rendered; a block without it replaces the parent's whole block; other overridden blocks stay excluded; a chain of templates keeps exactly the parents each level renders, once.
A template extending another replaced every block it defined in the parent, so the parent's filter applications dropped out of the render context flow even when the child rendered {{ block.super }}, which Django executes. A template now records the blocks that render block.super, and only the blocks defined without it are passed up as replaced, level by level.
A deployment declaration names its handler as the module binds it, and a module may bind that name as an alias of the function doing the work: main = actual. A model naming the alias's project symbol now applies to the function, or to the class's methods for a class alias. Aliases are module-level assignments of one name to another, followed transitively in source order; a name bound inside a function or class, or to a call, an import or any other value, is no alias.
…-ins

A project tag library registering a filter of a built-in's name replaces it in every template loading the library before applying the filter, as Django's parser does; loads are ordered, selective loads honoured, Django's own libraries change nothing. A library the engine cannot find, read or understand, or several apps disagreeing, leaves the filter uncertain: no call is claimed and the load is listed among the unread templates, keeping the advisory under investigation (#183).
@SizzleUnrlsd SizzleUnrlsd added the bug Something isn't working label Sep 28, 2026
@SizzleUnrlsd SizzleUnrlsd self-assigned this Sep 28, 2026
@SizzleUnrlsd
SizzleUnrlsd merged commit d3e9a64 into main Sep 29, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P1] Respect custom Django filter overrides before reporting vulnerable built-ins

1 participant