Summary
djust: Six template-layer defects emit attacker-controlled markup unescaped (XSS)
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. <img src=x onerror=alert(1)>'
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 '&lt;img src=x onerror=alert(1)&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 '<img src=x onerror=alert(1)>'
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.
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.
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.
Untrusted input is rendered as active markup in a victim's browser, which can run script in their session. Typical impact: session or credential theft, and actions taken as the user.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
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.
Frequently Asked Questions
- What is GHSA-9395-2G46-RJ3F? GHSA-9395-2G46-RJ3F is a high-severity cross-site scripting (XSS) vulnerability in djust (pip), affecting versions <= 1.1.0. It is fixed in 1.1.1. Untrusted input is rendered as active markup in a victim's browser, which can run script in their session.
- Which versions of djust are affected by GHSA-9395-2G46-RJ3F? djust (pip) versions <= 1.1.0 is affected.
- Is there a fix for GHSA-9395-2G46-RJ3F? Yes. GHSA-9395-2G46-RJ3F is fixed in 1.1.1. Upgrade to this version or later.
- Is GHSA-9395-2G46-RJ3F exploitable, and should I be worried? Whether GHSA-9395-2G46-RJ3F is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether GHSA-9395-2G46-RJ3F is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix GHSA-9395-2G46-RJ3F? Upgrade
djustto 1.1.1 or later.