Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐍 PyPI
Not in CISA KEV

GHSA-9395-2g46-rj3f djust

GHSA-9395-2g46-rj3f is a security vulnerability in djust. A fix is available for djust — see the affected versions and patch details below.

djust: Six template-layer defects emit attacker-controlled markup unescaped (XSS)

Published
Sep 17, 2026
Updated
Sep 17, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Sep 17, 2026 · OSV.dev, FIRST.org (EPSS)

Real-World Exposure

1 pkg affected
🐍djust

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.

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 = '<img src=x onerror=alert(1)>'
  djust   '1. <img src=x onerror=alert(1)>'      <- executes
  django  '1. &lt;img src=x onerror=alert(1)&gt;'

The filter deferred all escaping to render time; a trailing |safe suppressed exactly that. The exposure is wider than the |safe form: any downstream filter that reads the output as markup is affected, including {{ p|linenumbers|truncatechars_html:"5" }}, which contains no |safe at all.

2. escape was a no-op (#2281)

{{ p|escape|safe }}
  djust   '<img src=x onerror=alert(1)>'          <- executes
  django  '&amp;lt;img src=x onerror=alert(1)&amp;gt;'

Django's escape is eager (conditional_escape, returning SafeString). djust's returned its input unchanged and let the render site escape it — indistinguishable for {{ p|escape }} alone, wrong for every chain. The security cell is {{ p|escape|safe }}: an idiom that reads as "escape it, then it is safe to emit" — which is what Django's semantics make true — was a bare |safe on attacker input. A sweep of every length-2 and length-3 chain containing escape found 104 live-markup cells.

3. unordered_list / safeseq handed a string back under a safe grant (#2274)

Both carry an unconditional "emit without escaping" grant, earned because they escape every item they emit. Given a string rather than a sequence they emitted nothing and returned the input verbatim under that same grant, making {{ hostile|safeseq }} an exact synonym for |safe with no mark_safe anywhere in the template.

4. A safety grant outlived the value it was granted for (#2300)

No filter chain and no |safe anywhere; a bare {{ p }} is the whole reproducer.

Safe context keys accumulated on the view and were never revoked, so a key marked safe once stayed safe for the lifetime of the view — which spans every event on a WebSocket connection:

render 1:  p = mark_safe('<b>trusted</b>')     ->  '<b>trusted</b>'   correct
render 2:  p = '<img src=x onerror=alert(1)>'  ->  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 = '<img src=x onerror=alert(1)>'

  djust   '<img src=x onerror=alert(1)>'          <- executes
  django  '&lt;img src=x onerror=alert(1)&gt;'

Together with defect 3 this is one of the two classes reachable without the application writing anything unusual.

6. linebreaks / linebreaksbr — and |safe was the only spelling that worked (#2284)

linebreaks emits <p>/<br> but neither escaped its content nor reported its output safe. The plain spelling therefore escaped the filter's own tags and printed a literal <p> on the page, so |safe was the only form that rendered at all — and that form emitted the content live:

{{ bio|linebreaks }}         renders literal '<p>' text        (visibly broken)
{{ bio|linebreaks|safe }}    '<img src=x onerror=alert(1)>'    <- 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_safed 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.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐍PyPIdjustall versions1.1.1pip install --upgrade 'djust==1.1.1'

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for djust, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update djust to 1.1.1 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-9395-2g46-rj3f is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-9395-2g46-rj3f can be triaged on real exposure rather than presence alone.

Tailored to GHSA-9395-2g46-rj3f. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

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|
O3 Security · Impact-Aware SCA

Is GHSA-9395-2g46-rj3f in your dependencies?

O3 Security finds GHSA-9395-2g46-rj3f across PyPI dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.

GHSA-9395-2g46-rj3f: djust XSS | O3 Security