A niche surveyor's tool left its back door unlocked, and now burglars are testing the handle
CVE-2026-1207 is a SQL injection in Django's django.contrib.gis module, specifically in raster lookups on RasterField — a feature implemented only on PostgreSQL with the PostGIS extension. When an application passes user-controlled input as the *band index* parameter in a raster lookup, Django inlines that value directly into the SQL string instead of binding it as a parameterized value. An attacker who controls the band index can inject arbitrary SQL — demonstrated with pg_sleep() for time-based blind extraction, but extendable to full data exfiltration or modification. Affected versions: Django 6.0a1 – 6.0.1, 5.2a1 – 5.2.10, 4.2a1 – 4.2.27; unsupported series (5.0.x, 5.1.x, 4.1.x, 4.0.x, 3.2.x) are also vulnerable. Patches shipped February 3, 2026 in 6.0.2, 5.2.11, and 4.2.28. HeroDevs also backported a fix to 3.2.27 for their NES customers.
The vendor-provided NVD CVSS of 5.4 MEDIUM (C:L/I:L/A:N) significantly understates the real impact. Django's own security team rated this HIGH and the GHSA advisory scored it 8.1 under CVSS v4. The NVD's C:L/I:L is inaccurate for a demonstrated arbitrary-SQL-execution primitive — the PoC shows pg_sleep(), which means the attacker has broken out of the intended query entirely and can execute *any* SQL the database user is authorized to run, including COPY, pg_read_file, or stacked queries depending on PostgreSQL configuration. Meanwhile, CrowdSec confirmed active exploitation in the wild since February 26, 2026, with steady week-over-week probing volumes targeting ?band= endpoints. The narrow blast radius (PostGIS + RasterField + user-controlled band index) is the only thing keeping this from CRITICAL.
5 steps from start to impact.
Identify a Django + PostGIS target
X-Frame-Options: DENY default header, admin panel, or error page leakage) and determines PostGIS is in use. Shodan/FOFA dorks for Django GIS apps or reconnaissance of geospatial API endpoints (e.g., /api/raster/, /map/tiles/) reveals candidates. CrowdSec telemetry confirms attackers are already performing this targeted reconnaissance at scale.- Target application is Internet-reachable or attacker has internal network access
- Django application uses PostGIS backend with RasterField
- The vast majority of Django deployments do not use PostGIS at all — estimated <5% of all Django apps
- Among PostGIS users, RasterField is a niche feature for satellite/elevation/terrain data — further narrows to perhaps <1% of all Django installs
Locate an endpoint exposing the band index parameter
/?band=, /api/raster/search/?band=, or any GIS endpoint that accepts a band selector. The attacker needs a code path where user input flows into the band argument of a RasterField .filter() call without integer validation.- Application exposes a URL parameter, form field, or API argument that maps to the band index
- Application does not validate band index as a strict integer before passing it to the ORM
- Many GIS applications hardcode band index or derive it from server-side logic, never exposing it to user input
- Applications with input validation (even basic
int()casting) are immune
CVE-2026-1207.yaml from ProjectDiscovery detects vulnerable endpoints.Authenticate (if required)
- If PR:L applies: attacker has or obtains a low-privilege account on the Django application
- Applications behind SSO/MFA raise the bar for credential acquisition
- Public-facing GIS tile endpoints may bypass this friction entirely
Inject SQL via band index
?band=1);select+'1'||pg_sleep(3)-- to confirm injection via time delay. Tools: sqlmap with custom injection point (sqlmap -u 'http://target/search?band=1*' --dbms=PostgreSQL --technique=T), or the sw0rd1ight/CVE-2026-1207 PoC directly. The injected SQL executes in the context of the PostgreSQL user Django connects as — typically with full read/write access to the application database.- Steps 1-3 satisfied
- PostgreSQL accepts the injected syntax (standard for PostGIS setups)
- Database connection user with restricted permissions limits blast radius
- PostgreSQL
log_statement = 'all'creates audit trail
band parameter. Database query logging for anomalous pg_sleep, UNION SELECT, or stacked queries. IDS/IPS rules for time-based SQLi patterns.Exfiltrate data or escalate
SUPERUSER or pg_read_server_files role, the attacker can read files from the server filesystem (pg_read_file()). Extracted credentials may enable lateral movement if password reuse exists.- Successful SQL injection from step 4
- Database contains valuable data or credentials
- Time-based blind extraction is slow — full table dumps take hours and generate detectable traffic
- Properly segmented database with no credential reuse limits lateral movement
- Column-level encryption on sensitive fields defeats bulk extraction
int() or rejects non-integer values *before* it reaches the Django ORM. This is the single most effective compensating control because it breaks the injection primitive entirely — no valid integer can contain SQL syntax. Deploy within 30 days per the noisgate mitigation SLA for HIGH severity. Example: band = int(request.GET.get('band', 1)) wrapped in a try/except that returns 400 on failure.band query parameter specifically, and in all query parameters generally. CrowdSec shared blocklists already flag IPs scanning for this CVE. Deploy within 30 days per the noisgate mitigation SLA. Note: WAFs can be bypassed with encoding tricks, so this is defense-in-depth, not a standalone fix.SUPERUSER, no pg_read_server_files, no COPY to filesystem. Use row-level security where possible. This does not prevent injection but limits the blast radius to the application's own tables. Deploy within 30 days.- Rate limiting alone — SQL injection needs only one successful request to confirm the vulnerability and begin data extraction. Rate limiting slows bulk extraction but does not prevent the initial compromise or targeted single-query exfiltration.
- Generic CSRF protection — Django's CSRF middleware protects against cross-site request forgery, not against an attacker directly sending requests to the vulnerable endpoint. CSRF tokens do not validate query parameter content.
- Switching to a non-PostGIS GeoDjango backend — While technically this removes the vulnerable code path (RasterField is PostGIS-only), it breaks all raster functionality and is not a viable compensating control for applications that depend on raster data.
The supporting signals.
| In-the-Wild Exploitation | Confirmed active. CrowdSec observed first attacks on February 26, 2026 — 23 days after patch release. Steady week-over-week probing volumes targeting ?band= endpoints. Characterized as targeted reconnaissance against PostGIS setups, not mass spraying. |
|---|---|
| Proof-of-Concept | Multiple PoCs available. sw0rd1ight/CVE-2026-1207 — full Django app demonstrating time-based blind SQLi via pg_sleep(). ProjectDiscovery nuclei template for automated scanning. Listed on pocindex.io with 1 GitHub repo + 1 nuclei template. |
| EPSS | 0.12819 (12.8%) — 96th percentile. This places CVE-2026-1207 in the top 4% of all CVEs by predicted exploitation probability, consistent with the confirmed in-the-wild activity. |
| CISA KEV Status | Not listed as of 2026-10-06. Despite confirmed exploitation, CISA has not added this to the Known Exploited Vulnerabilities catalog. |
| CVSS Vector Analysis | NVD: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N = 5.4 MEDIUM. GHSA CVSSv4: 8.1 HIGH. Django's own security policy: HIGH. The NVD C:L/I:L rating is arguably inaccurate — demonstrated arbitrary SQL execution should warrant C:H/I:H, yielding ~8.1 under v3.1 as well. |
| Affected Versions | Django 6.0a1–6.0.1, 5.2a1–5.2.10, 4.2a1–4.2.27. Unsupported: 5.0.x, 5.1.x, 4.1.x, 4.0.x, 3.2.x (HeroDevs patched 3.2.27 for NES customers). |
| Fixed Versions | 6.0.2, 5.2.11, 4.2.28 (released 2026-02-03). Debian: python3-django updated via DSA. Red Hat: patches for Ansible Automation Platform, Satellite, Discovery, Insights. HeroDevs: 3.2.27 for NES. |
| Scanning / Exposure | CrowdSec shared intelligence feeds flag IPs targeting this CVE. No dedicated GreyNoise tag identified. Shodan queries for Django + PostGIS show limited public exposure — most PostGIS-backed Django apps are internal or behind CDN/reverse proxy. |
| Disclosure | Reported by Tarek Nakkouch. Disclosed 2026-02-03 via coordinated Django security release. Commits: 81aa529 (4.2.x), 17a1d64 (5.2.x), 8f77e73 (6.0.x). |
| Red Hat Assessment | Classified as Important. Affects multiple products: Ansible Automation Platform, Satellite, Discovery, Insights — wherever Django is bundled with PostGIS raster capabilities. |
Sources.
- Django Security Release Announcement
- GHSA-mwm9-4648-f68q — GitHub Security Advisory
- CrowdSec VulnTracking Report — Active Exploitation
- HeroDevs — SQL Injection in Django Raster Lookups
- sw0rd1ight/CVE-2026-1207 — PoC Repository
- ProjectDiscovery Nuclei Template
- SecurityOnline — Django SQLi Exploited in the Wild
- Django Commit Fix (4.2.x branch)
Why this verdict
- Active exploitation overrides narrow surface. CrowdSec confirmed steady in-the-wild probing since February 26, 2026. When attackers are already pulling the trigger, theoretical friction loses weight — the question shifts from 'could someone exploit this?' to 'are they hitting *your* deployment?'
- NVD CVSS dramatically underscores impact. The 5.4 rating uses C:L/I:L, but the demonstrated PoC achieves arbitrary SQL execution (
pg_sleep, stacked queries). A correct assessment of the injection primitive is C:H/I:H, which yields ~8.1 under CVSS v3.1 — aligning with Django's own HIGH rating and the GHSA v4 score of 8.1. - Public PoC + nuclei template = low attacker skill floor. sw0rd1ight's full Django app PoC and ProjectDiscovery's nuclei scanner template mean any script kiddie with
nuclei -t CVE-2026-1207.yamlcan find and exploit vulnerable targets. Combined with sqlmap for automated extraction, weaponization is trivial. - EPSS 96th percentile validates real-world signal. At 12.8%, EPSS ranks this in the top 4% of all CVEs — a strong independent indicator that the exploitation characteristics (network vector, low complexity, public PoC) make this a practical attack, not a theoretical one.
- Role multiplier: production database tier. Django+PostGIS deployments with RasterField are typically geospatial analysis platforms — government mapping, environmental monitoring, defense/intelligence GIS, precision agriculture. These often sit on production PostgreSQL databases containing regulated or sensitive geospatial data (property records, critical infrastructure locations, classified imagery metadata). The blast radius on a successful SQLi is full database read/write, which in these contexts means sensitive data exfiltration. However, this is database-level impact, not fleet/domain/supply-chain scale. Django is NOT a canonically high-value-role component (not an IdP, hypervisor, CA, PAM system, etc.), and the PostGIS+RasterField subset represents well under 1% of all Django deployments, so the HIGH floor (domain/fleet compromise) does not mechanically trigger. The upgrade to HIGH is driven by active exploitation + understated CVSS, not by the role-multiplier floor.
- Friction acknowledged but insufficient for MEDIUM. Yes, PostGIS + RasterField + user-controlled band index is a narrow surface. But: (1) attackers are already scanning for exactly this combination, (2) the organizations *most likely* to use PostGIS RasterField are high-value targets (government, defense, critical infrastructure), and (3) a single successful SQLi on one of these targets justifies the upgrade.
Why not higher?
Not CRITICAL because the exploitable surface is extremely narrow — the chain requires PostGIS backend + RasterField model + user-controlled band index flowing into ORM lookups without validation. This is likely <1% of all Django installations. Django is a general-purpose web framework, not a canonically high-value-role component (it is not an identity provider, hypervisor, certificate authority, or domain controller). Even when the chain succeeds, the blast radius is database-level (read/write the app's PostgreSQL instance), not fleet-scale or domain-scale. There is no evidence of wormable propagation or supply-chain impact.
Why not lower?
Not MEDIUM because active in-the-wild exploitation is confirmed by CrowdSec with steady week-over-week volumes since February 2026. The NVD CVSS of 5.4 is provably inaccurate — the PoC demonstrates arbitrary SQL execution, not the limited C:L/I:L the vector claims. EPSS at the 96th percentile independently corroborates high exploitability. Public PoCs and a nuclei template make weaponization trivial. The organizations most likely to run PostGIS RasterField (government GIS, defense, critical infrastructure) are high-value targets where SQLi has outsized consequences.
Crowdsourced verification payload.
Run this script on each host or in each virtualenv/container where Django is installed. Invoke with python3 cve_2026_1207_check.py. No special privileges required — it only reads the installed Django package version. For fleet-wide checks, push via your config management tool (Ansible, Salt, Chef) or scan container images with pip show django in CI.
#!/usr/bin/env python3
"""CVE-2026-1207 version checker — Django PostGIS RasterField SQL Injection.
Outputs: VULNERABLE | PATCHED | UNKNOWN
Exit codes: 1 = vulnerable, 0 = patched, 2 = unknown
"""
import sys
import subprocess
import re
def get_django_version():
"""Try import first, fall back to pip show."""
try:
import django
return django.get_version()
except ImportError:
pass
try:
out = subprocess.check_output(
[sys.executable, "-m", "pip", "show", "django"],
stderr=subprocess.DEVNULL, text=True
)
for line in out.splitlines():
if line.startswith("Version:"):
return line.split(":", 1)[1].strip()
except Exception:
pass
return None
def ver(s):
"""Parse version string to comparable tuple."""
parts = re.match(r"(\d+)\.(\d+)\.?(\d+)?", s)
if not parts:
return None
return (int(parts.group(1)), int(parts.group(2)), int(parts.group(3) or 0))
def check(v):
"""Return True if version is vulnerable."""
# Fixed: 6.0.2, 5.2.11, 4.2.28
# Vulnerable ranges:
# 6.0.0 - 6.0.1
# 5.2.0 - 5.2.10
# 4.2.0 - 4.2.27
# All unsupported series below 4.2 or between supported series
if v >= (6, 0, 0) and v < (6, 0, 2):
return True
if v >= (5, 2, 0) and v < (5, 2, 11):
return True
if v >= (4, 2, 0) and v < (4, 2, 28):
return True
# Unsupported series: 5.0.x, 5.1.x assumed vuln (no patch)
if v >= (5, 0, 0) and v < (5, 2, 0):
return True
# 4.0.x, 4.1.x — unsupported, assumed vuln
if v >= (4, 0, 0) and v < (4, 2, 0):
return True
# 3.x and below — unsupported, assumed vuln (unless HeroDevs 3.2.27+)
if v < (4, 0, 0):
# HeroDevs patched 3.2.27; check for that
if v >= (3, 2, 27):
return False # HeroDevs NES patch
return True
return False
def main():
version_str = get_django_version()
if version_str is None:
print("UNKNOWN - Django not found in this Python environment")
sys.exit(2)
v = ver(version_str)
if v is None:
print(f"UNKNOWN - Could not parse Django version: {version_str}")
sys.exit(2)
if check(v):
print(f"VULNERABLE - Django {version_str} is affected by CVE-2026-1207")
print(" Fix: upgrade to 6.0.2, 5.2.11, or 4.2.28+")
# Additional context: check if PostGIS is even in use
try:
from django.conf import settings
if hasattr(settings, 'DATABASES'):
for alias, db in settings.DATABASES.items():
if 'postgis' in db.get('ENGINE', '').lower():
print(f" WARNING: database '{alias}' uses PostGIS — exploitation is possible")
except Exception:
print(" Note: could not inspect DATABASES setting (run within Django context for full check)")
sys.exit(1)
else:
print(f"PATCHED - Django {version_str} is not affected by CVE-2026-1207")
sys.exit(0)
if __name__ == "__main__":
main()