A SQL injection hiding in a GIS corner of Django that most defenders forgot they shipped
CVE-2026-1207 is a classic string-concatenation SQLi (CWE-89) in Django's GIS raster lookup path. When an application uses RasterField on PostGIS and passes a user-controlled *band index* into a raster lookup, Django inlines that integer into the generated SQL rather than binding it as a parameter. An authenticated user who controls that value can break out of the intended position and inject arbitrary SQL against the backing PostgreSQL/PostGIS database. Fixed versions are 6.0.2, 5.2.11, and 4.2.28, all shipped 2026-02-03; older unsupported branches (3.2.x, 4.1.x, 5.0.x) were not evaluated but are known-vulnerable per HeroDevs, which shipped a backport for 3.2.27.
The vendor's MEDIUM/5.4 rating leans on the PR:L and C:L/I:L scope — authenticated, partial impact. That framing understates two things: (1) SQLi against a production PostGIS instance is *rarely* actually partial — union-based extraction and second-order writes routinely escalate to full schema egress and tampering; (2) CrowdSec confirmed in-the-wild exploitation from 2026-02-26 onward, with sustained week-over-week scanning of /?band= and /api/raster/search/?band= patterns. Vendor severity does not match reality — this belongs in the HIGH bucket for any org actually running GeoDjango + PostGIS.
5 steps from start to impact.
Identify a Django + PostGIS target with a RasterField endpoint
/?band=, /api/raster/search/?band=, and similar query strings. Tools in use: httpx, nuclei templates, and custom crawlers looking for map/tile/raster APIs.- Django application reachable by the attacker
- App uses
django.contrib.giswith PostGIS backend - At least one view exposes a raster lookup where a band index maps to a request parameter
- GeoDjango RasterField is a niche feature — only apps doing geospatial raster analytics use it
- Most Django apps route GIS through vector fields, not raster
- Read-only or cached tile services often bypass the ORM path entirely
band= params; Nuclei has community templates; WAF signatures for UNION SELECT in band parameter catch naive payloads.Obtain a low-privileged authenticated session
PR:L is not decorative — the vulnerable code paths require an authenticated Django user. The attacker registers (if signup is open), uses stolen creds, or leverages a companion IDOR/auth-bypass. sqlmap with --cookie and --csrf-token is the workhorse here.- Valid session cookie or API token for the target Django app
- The authenticated role has permission to hit the raster-lookup view
- Enterprise apps behind SSO/MFA raise the cost of session acquisition
- Rate limiting on the vulnerable endpoint slows blind exfil
- Some deployments gate GIS features to internal staff only
Inject via the band index parameter
band value that closes the integer context and appends SQL — e.g. 1) UNION SELECT current_user, version(), NULL --. Because Django inlines the value instead of binding it, PostGIS executes the appended statement. sqlmap -u ... --technique=U --dbms=postgresql automates enumeration.- Endpoint reaches the vulnerable
RasterFieldlookup with attacker-controlled band index - No integer-only allowlist / type coercion sits in front of the lookup
- Many apps wrap band selection in a form or serializer that forces
IntegerFieldvalidation — that kills the payload before it reaches the ORM - PostGIS role separation may deny the app user access to sensitive schemas
log_statement=all catches the injected SQL; WAFs with SQLi signatures on numeric params catch string-typed payloads; Django DEBUG=False still leaks 500s that indicate injection surface.Enumerate schema and exfiltrate
information_schema.tables, pg_user, application session tables, and password hashes. On a chatty error page or via time-based blind, the attacker can pivot to pg_read_server_files if the DB role permits, or to writing rows if the app user has INSERT/UPDATE. Practical tooling: sqlmap --dump-all, custom UNION harnesses.- DB user has SELECT on target tables (typical for the Django app role)
- Attacker can iterate requests without lockout
- Least-privilege DB roles (no
pg_read_server_files, noCOPY) cap severity to the app's own schema - Row-level security policies further constrain what the injected query can read
information_schema reads; DAM tools (Imperva, DataDog DB Monitoring) flag.Pivot to auth-token / credential abuse
django_session rows, or Argon2/PBKDF2 password hashes feed offline cracking and account takeover. If the app stores API keys or OAuth refresh tokens, the attacker escalates laterally to whatever those keys unlock — the SQLi itself doesn't need code exec if it hands you the keys to the kingdom.- Sensitive material stored in the reachable schema
- Hashed passwords with modern KDFs resist cracking
- Secrets stored in a vault (not the DB) neutralize this pivot
The supporting signals.
| In-the-wild status | Active exploitation confirmed by CrowdSec starting 2026-02-26; steady week-over-week probing, characterized as *targeted* rather than opportunistic spray |
|---|---|
| KEV status | Not KEV-listed as of 2026-07-10; CrowdSec and multiple trackers expect addition |
| EPSS | 0.09436 (~9.4%) — elevated for a MEDIUM-labeled CVE; consistent with observed exploitation |
| Proof-of-concept | No public weaponized PoC in mainstream repos as of writing; sqlmap handles this class trivially once the endpoint is known. Attack traffic patterns published by CrowdSec |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N = 5.4 MEDIUM — vendor rates impact as *partial*; real-world SQLi impact is typically full DB read |
| Affected versions | Django < 4.2.28, 5.2 < 5.2.11, 6.0 < 6.0.2; unsupported 3.2.x, 4.1.x, 5.0.x also vulnerable (HeroDevs confirmed 3.2) |
| Fixed versions | 4.2.28, 5.2.11, 6.0.2 (2026-02-03); HeroDevs NES 3.2.27 (2026-04-29); Red Hat backport tracked in RHBZ 2436338 |
| Preconditions narrowing exposure | Requires django.contrib.gis + PostGIS + RasterField lookup + user-controllable *band index* + authenticated session — GIS raster is a niche subset of Django |
| Disclosure | 2026-02-03 by Django Security Team, credited via Django security policy channel |
| Scanner probes seen | ?band=, /api/raster/search/?band= — CrowdSec published detection scenarios and IP list within 8 days of disclosure |
noisgate verdict.
The single most decisive factor is active in-the-wild exploitation combined with a data-tier blast radius: successful SQLi against PostgreSQL/PostGIS routinely converts vendor-labeled 'partial' confidentiality into full-schema egress including session tokens and password hashes. Vendor's MEDIUM rating undervalues the impact ceiling of unauthenticated-equivalent SQLi on a production database once a low-priv session is obtained.
Why this verdict
- Active exploitation: CrowdSec confirmed real attack traffic from 2026-02-26 — this is no longer theoretical.
- Impact underscored: SQLi against PostGIS with the Django app's DB role rarely stays 'partial' — union-based reads escalate to session/credential theft, which is why EPSS is already ~9.4%.
- Role multiplier: The affected component is a Django *application tier* speaking to a *production database* — the blast radius from a successful chain is fleet-scale within the tenant (all rows accessible to the app user), and Django is one of the most widely deployed Python web frameworks. Not identity/hypervisor tier, but data-tier compromise plausibly ends in mass PII/credential egress.
- Friction limiting upgrade to CRITICAL:
PR:Lrequires authenticated access, and the vulnerable code path is only reachable in apps using GeoDjango + PostGIS +RasterFieldwith user-controlled band index — a small fraction of Django's installed base. - Deployment-role floor: Django apps hosting regulated/PII data on PostgreSQL are canonically 'production database' tier. Any org running GeoDjango + PostGIS for real (geospatial platforms, insurance/agri/logistics analytics) hits the HIGH floor by role.
Why not higher?
CRITICAL requires either unauthenticated exploitation, a canonical high-value-role component (hypervisor/IdP/PAM/DC), or KEV listing. This CVE requires PR:L, the vulnerable code path is a niche feature, and CISA has not yet added it. Also, well-configured deployments already coerce band index via IntegerField in serializers, which entirely neutralizes the payload.
Why not lower?
MEDIUM undersells two facts: exploitation is *actively happening in the wild*, and the impact vector is SQLi against a production database — that outcome rarely stays partial once an attacker enumerates the schema. Any framework tracking real-world signals (EPSS ~9.4%, CrowdSec traffic) pushes this past MEDIUM.
What to do — in priority order.
- Coerce band index to
intat the view/serializer boundary — Wrap every raster lookup input with DRFIntegerField/ Djangoforms.IntegerFieldor an explicitint()cast with a validated range. This kills the payload before it reaches the ORM. Deploy within 30 days per noisgate mitigation SLA — same day if you can identify the endpoints. - Deploy a WAF rule blocking non-integer values on
bandand adjacent raster params — Signature: reject anyband(or similarly-named) parameter that doesn't match^-?\d{1,3}$. F5, Cloudflare, AWS WAF, and ModSecurity CRS can all express this. Buys you time until the Django upgrade lands. - Rotate the Django app's PostgreSQL role to least privilege — The app user should have no
pg_read_server_files, noCOPY FROM PROGRAM, and only SELECT/INSERT/UPDATE on its own schema. Row-level security on tables holding PII/tokens caps the blast radius even if SQLi lands. - Enable
log_statement='mod'or full statement logging on PostgreSQL for the app role — Feeds a SIEM detection forUNION SELECT,information_schema.tablesreads, andpg_userreads originating from the Django app user. - Rotate session keys and force reauth if you detect probes — If CrowdSec/WAF logs show real band-parameter injection attempts, assume session-cookie exfil and rotate
SECRET_KEY+ invalidate all sessions.
- Django
DEBUG=False— hides errors but does not stop the injection itself; blind SQLi still works. - Prepared statements at the psycopg2 layer — the ORM does the concatenation *before* handing SQL to the driver; the driver has no chance to parameterize.
- CSRF tokens — the attacker is authenticated and can obtain a fresh CSRF token as part of the normal auth flow.
- Rate limiting alone — targeted attackers pace their requests; and even one successful UNION exfil per minute drains a small schema in a workday.
Crowdsourced verification payload.
Run on any host that has your Django project's requirements.txt / poetry.lock / installed venv. Invoke as python check_cve_2026_1207.py /path/to/venv/bin/python (pass the interpreter of the environment you want to test) or omit the arg to test the current interpreter. No elevated privileges required.
#!/usr/bin/env python3
# noisgate — CVE-2026-1207 detector (Django GIS RasterField SQLi)
# Exits 0 = PATCHED, 1 = VULNERABLE, 2 = UNKNOWN
import subprocess, sys, re
FIXED = {
(4, 2): (4, 2, 28),
(5, 2): (5, 2, 11),
(6, 0): (6, 0, 2),
}
def get_django_version(py):
try:
out = subprocess.check_output(
[py, '-c', 'import django,sys;sys.stdout.write(django.get_version())'],
stderr=subprocess.STDOUT, timeout=10).decode().strip()
return out
except Exception as e:
print(f'UNKNOWN: could not import django ({e})')
sys.exit(2)
def parse(v):
m = re.match(r'^(\d+)\.(\d+)\.(\d+)', v)
if not m:
return None
return tuple(int(x) for x in m.groups())
def main():
py = sys.argv[1] if len(sys.argv) > 1 else sys.executable
ver = get_django_version(py)
parsed = parse(ver)
if not parsed:
print(f'UNKNOWN: unparseable Django version {ver}')
sys.exit(2)
major, minor, patch = parsed
# Unsupported branches (3.2, 4.1, 5.0) are vulnerable unless HeroDevs NES applied
if (major, minor) in [(3, 2), (4, 1), (5, 0)]:
print(f'VULNERABLE: Django {ver} is on an unsupported branch; upstream did not evaluate. See HeroDevs NES for 3.2.')
sys.exit(1)
fixed = FIXED.get((major, minor))
if fixed is None:
print(f'UNKNOWN: Django {ver} branch not in fix table')
sys.exit(2)
if parsed >= fixed:
print(f'PATCHED: Django {ver} >= {".".join(map(str, fixed))}')
sys.exit(0)
print(f'VULNERABLE: Django {ver} < {".".join(map(str, fixed))} — CVE-2026-1207')
sys.exit(1)
if __name__ == '__main__':
main()
If you remember one thing.
django.__version__ — the verification script above does this in one line; (2) for any app using django.contrib.gis with PostGIS, ship input coercion on band-index parameters and a WAF signature *today* — the noisgate mitigation SLA is ≤ 30 days for HIGH, but with active exploitation the effective clock is *hours*; (3) schedule the actual Django upgrade to 4.2.28 / 5.2.11 / 6.0.2 (or HeroDevs NES 3.2.27 if you're stranded) — the noisgate remediation SLA is ≤ 180 days for HIGH, but frankly any Django shop can land a point release inside a normal 2-week sprint. If your CrowdSec/WAF logs show real band= injection attempts pre-patch, rotate SECRET_KEY, invalidate sessions, and audit django_session and any tables holding tokens or password hashes.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.