djust: mark_safe Context Key Inheritance XSS
djust's template engine tracks which variable names are HTML-safe by name rather than by value, so rebinding a safe name to attacker-controlled input causes raw HTML to be rendered without escaping.
The problem
djust's Rust template engine maintains safety grants in a set keyed by variable name, not by value. When a view marks a value safe under a name like p, that name is added to safe_keys.
Any template construct that rebinds that name ({% with %}, {% for %}, {% include ... with %}, assign tags) copies the new value in but leaves the grant in place. The renderer then treats the new, attacker-controlled value as trusted and skips escaping entirely.
Proof of concept
A working proof-of-concept for this issue in djust, with the exact payload below.
# View
from django.utils.safestring import mark_safe
def my_view(request):
context = {
"p": mark_safe("<b>trusted</b>"),
"user_input": request.GET.get("q", ""),
}
return render(request, "page.html", context)
# Template: page.html
# {% with p=user_input %}{{ p }}{% endwith %}
# Exploit URL:
# /page/?q=<img src=x onerror=alert(document.cookie)>
#
# djust renders: <img src=x onerror=alert(document.cookie)> <- executes
# Django renders: <img src=x onerror=alert(document.cookie)> <- safeThe root cause is in context.rs: safe_keys is an AHashSet<String> populated by _collect_safe_keys in rust_bridge.py, which records variable names for any SafeString in the context. In renderer.rs, the runtime safety check calls context.is_safe(var_name), a string-set lookup that ignores the current value bound to that name.
When {% with p=user_input %} executes, the Rust renderer pushes a new scope with the attacker string bound to p, but safe_keys still contains "p" from the parent scope. The check context.is_safe("p") returns true, so the raw attacker string is emitted without escaping.
The fix in 1.1.2 changes the bind semantics so that any rebinding of a name replaces rather than inherits its safety grant. A new scope bind must positively re-earn the grant from the incoming value, not carry the parent grant forward by name.
The fix
Upgrade to djust 1.1.2 or later (pip install -U djust). As a temporary workaround before upgrading, do not reuse a context name for both mark_safe-marked content and untrusted user input, and avoid rebinding such a name in {% with %}, {% for %}, {% include ... with %}, or an assign tag.
Related research
- highdjust: Six Template-Layer XSS Defects via Broken Auto-Escape
- high · 7.4CVE-2026-61592CVE-2026-61592: djust SSE Session Hijack via Client-Controlled session_id
- high · 8.1CVE-2026-61591CVE-2026-61591: djust Unsigned State Snapshot Privilege Escalation
- high · 7.7CVE-2026-61595CVE-2026-61595: djust Multi-Tenant Isolation Fails Open on WebSocket/SSE Path