GitHub Advisoryadvisory · PyPI
GHSA-9395-2g46-rj3f djust: Six template-layer defects emit attacker-controlled markup unescaped (XSS)
HIGH
Official page ↗
- Ecosystem
- PyPI
- Packages
- djust
- Severity
- High
- Weaknesses
- CWE-116, CWE-79
- Reviewed
- 2026-09-17
Affected packages
• PyPI djust: < 1.1.1 → fixed in 1.1.1
Description
Five independent defects in djust's template auto-escaping cause attacker-controlled input to be rendered as live markup where Django escapes it. All four are present in shipped 1.1.0 and are fixed in 1.1.1.
They share one shape: a filter or grant that escapes nothing itself and relies on the render-time auto-escape, which something downstream then removes. They are grouped into a single advisory because the mitigation is identical — upgrade to 1.1.1 — and because no single one of them is meaningfully actionable in isolation.
1. `linenumbers` never escaped its input (#2291)
```
{{ p|linenumbers|safe }} with p = ''
djust '1. ' ' trusted') -> 'trusted' correct
render 2: p = '' -> executes
```
A view that renders trusted markup into a variable and later renders user input into the same variable emits it live.
5. A custom tag handler's return was emitted raw — including djust's own `{% render_slot %}` (#2379)
Reachable with no `|safe`, no `mark_safe`, and no application code: using component slots is enough.
Django's `SimpleNode.render` runs `conditional_escape` over a `simple_tag`'s return
unless it carries `__html__`. djust inserted the return verbatim, so a handler as
ordinary as `return f"Hello {name}"` emitted attacker markup live.
Of the 221 handlers djust registers, one echoes a context value unescaped —
`render_slot`, the framework's own function-component/slot tag:
```
{% render_slot p %} p = ''
djust '' `/`` but neither escaped its content nor reported its
output safe. The plain spelling therefore escaped the filter's own tags and
printed a literal `` on the page, so `|safe` was the only form that rendered
at all — and that form emitted the content live:
```
{{ bio|linebreaks }} renders literal '' text (visibly broken)
{{ bio|linebreaks|safe }} '' <- executes
```
Because the broken spelling is the one a developer discards, the vulnerable
spelling is the one that ships. Any application rendering user-entered text with
paragraph breaks is written that way.
Impact
Stored or reflected XSS in any djust application that renders untrusted input through the affected filters, or that reuses a context variable which was previously marked safe. Exploitation requires no special configuration. Defects 3, 4 and 5 require no unusual template construct at all — defect 5 needs only that the application use component slots, and defect 6's vulnerable spelling is the only one that renders correctly.
Patches
Fixed in 1.1.1, and in 1.2.0 (`main`).
1.1.1 re-implements each fix against 1.1.0's own code rather than back-porting `main`'s, which depends on a value-level safety model 1.1.0 does not have. Three consequences are documented in the 1.1.1 CHANGELOG and are all in the over-escaping direction: `{{ p|escape|F }}` double-escapes for a plain following filter `F`; `{% render_slot slot.content %}` over-escapes; and a `mark_safe`d value passed through `|escape` is escaped rather than passed through.
Not fixed in 1.1.1, and tracked separately: application-written tag handlers that return attacker data as a plain `str` (only djust's own `render_slot` is covered by defect 5), and a `{% with %}`/`{% for %}` bind inheriting a safety grant it never earned. Both are fixed in 1.2.0.
Workarounds
None complete. Before upgrading, avoid `|safe` after any filter in a chain, avoid `safeseq`/`unordered_list` on values that may be strings, and avoid reusing a context variable for both `mark_safe` content and untrusted input.
Credit
Found during an internal Django-parity audit by a registry-wide differential that compares djust's escaping capabilities against Django's across every filter chain, rather than by inspection.