Django's GeoDjango module (django.contrib.gis) makes it easy to run spatial queries — distance calculations, intersections, buffering — directly through the ORM. In March 2020, the Django security team disclosed CVE-2020-9402, a SQL injection vulnerability in these GIS functions and aggregates when Django is configured to use an Oracle database backend. If an application passed untrusted input into the tolerance parameter accepted by certain GeoDjango spatial functions, an attacker could inject arbitrary SQL into the generated query, potentially leading to unauthorized data access, data modification, or full database compromise depending on the privileges of the database account in use.
This is a good case study in why "the ORM protects you from SQL injection" is a dangerous half-truth, and a concrete example of SQL injection surfacing in code most teams never look at twice. Django's ORM parameterizes queries by default, but backend-specific code paths — especially less-traveled ones like GeoDjango's Oracle spatial adapter — can carry their own escaping logic, and that logic can get it wrong. A generic sql injection test run against the application's main query paths would likely miss this entirely, since the flaw lives specifically in an optional, backend-specific adapter rather than the mainline ORM code most reviews and scanners focus on.
Affected Versions and Components
The vulnerability lives specifically in django.contrib.gis when the Oracle database backend is in use. It does not affect PostgreSQL/PostGIS, MySQL, or SQLite spatial backends, because the flaw is in how the Oracle backend's spatial adapter built SQL for functions and aggregates that accept a tolerance parameter — a value Oracle Spatial uses to control the precision of approximate geometric calculations (used by functions such as area, distance, and other geometry operations exposed through GeoDjango).
Rather than passing the tolerance value through Django's normal parameterized-query machinery, the Oracle backend interpolated it directly into the SQL string. If an application derived that tolerance value from user-supplied input (for example, exposing precision as a query parameter on a mapping or geospatial search endpoint), an attacker could supply a crafted value containing SQL syntax instead of a numeric tolerance, breaking out of the intended query structure — a clean example of SQL injection surfacing not from a missing parameterized query at the app layer, but from a backend adapter that silently reverted to string interpolation underneath an ORM developers trusted to handle it.
Django announced fixes in the following releases:
- Django 1.11.29 (patches the 1.11 LTS series)
- Django 2.2.11 (patches the 2.2 LTS series)
- Django 3.0.4 (patches the 3.0 series)
Any project running an earlier version of these release lines with GeoDjango and Oracle configured is affected. Projects that don't use django.contrib.gis, or that use it with a non-Oracle backend, are not impacted by this specific CVE.
The issue was identified and reported by Adam Johnson, a longtime Django core contributor known for his focus on Django security and best practices, which is part of why the fix landed cleanly and quickly through Django's established security disclosure process.
Severity: CVSS, EPSS, and KEV Context
NVD's entry for CVE-2020-9402 assigns a CVSS v3.1 base score of 8.8 (High), reflecting a network-exploitable issue with low attack complexity and no privileges required for the attacker, but one that does depend on an application actually exposing the tolerance parameter to user-controlled input — the vulnerability isn't exploitable purely by Django running with a vulnerable version in the abstract; it requires a specific application pattern on top of it.
That "requires a specific pattern" caveat matters in practice. This is not a mass-internet, unauthenticated, no-configuration RCE. It requires: (1) GeoDjango in use, (2) the Oracle database backend specifically, and (3) application code that plumbs untrusted input into a GIS function's tolerance argument. That combination is far less common than, say, a generic Django or PostgreSQL deployment, which is consistent with the relatively low EPSS (Exploit Prediction Scoring System) likelihood typically observed for this CVE — scoring data has generally placed it well down the list of vulnerabilities with observed or predicted real-world exploitation activity, compared to more broadly reachable Django CVEs.
CVE-2020-9402 does not appear on CISA's Known Exploited Vulnerabilities (KEV) catalog as of this writing, meaning there is no confirmed evidence of it being actively exploited in the wild. That should not be read as "safe to ignore," however — Oracle-backed GeoDjango deployments are common in government, utilities, and enterprise GIS platforms, exactly the kind of environment where legacy dependency versions tend to linger far past their patch date. A django gis sql injection issue with a niche configuration requirement can still represent a serious, exploitable path in a system that fits that configuration.
Disclosure Timeline
- Discovery: Adam Johnson identified that the Oracle backend for
django.contrib.giswas building SQL for tolerance-accepting GIS functions and aggregates by string interpolation rather than parameter binding, and reported it through Django's private security reporting channel. - Coordinated fix: The Django security team developed and validated a fix that correctly binds the tolerance value as a query parameter (and/or validates it as numeric) instead of interpolating it into the SQL string.
- March 4, 2020 — public disclosure and patch release: Django published security releases 1.11.29, 2.2.11, and 3.0.4, along with a security advisory assigning CVE-2020-9402 and describing the tolerance-parameter SQL injection issue in the Oracle GIS backend. Downstream Linux distributions and package maintainers picked up the patched Django releases shortly after, consistent with Django's standard security release process.
Because Django's security releases target its supported release series (rather than a single point release), the fix was available simultaneously across the two LTS branches then in support (1.11 and 2.2) as well as the current 3.0 series — reducing the "which version do I even upgrade to" friction that often slows adoption of security patches in the wild.
Remediation Steps
- Upgrade Django. Move to Django 1.11.29, 2.2.11, or 3.0.4 or later, matching whichever release series your project already tracks. If you're on an even older, unsupported Django series, this is a good forcing function to plan a broader upgrade, since older series no longer receive security patches at all.
- Confirm your GIS backend. If you're not using Oracle as your spatial database backend, you are not exposed to this specific CVE — but treat the discovery as a prompt to check GeoDjango configuration across all your Django services, since backend choice is easy to lose track of in large deployments.
- Audit tolerance parameter usage. Search your codebase for any GeoDjango GIS function or aggregate calls (e.g., functions taking a
tolerancekeyword argument) where the value originates from request data, query strings, or any other user-controllable source. Even after patching, treat this as a design smell — untrusted input shaping query precision parameters is worth revisiting. - Apply defense in depth regardless of patch status. Validate and type-check any numeric parameter before it reaches the ORM, even parameters you assume are "just numbers." A patched Django closes this specific gap, but the underlying lesson on how to avoid sql injection generally — validate input at the boundary, not just trust the framework — applies broadly. A WAF sql injection rule tuned to flag non-numeric characters in a parameter that should only ever be a float can also catch exploitation attempts against unpatched instances, though it's a compensating control, not a substitute for the upgrade.
- Re-run dependency and SCA scans after patching. Confirm the patched Django version is actually what's deployed across every environment and container image, not just merged into a requirements file. Transitive pins, cached wheels, and stale lockfiles are common reasons a "fixed" CVE keeps showing up in production long after the patch was merged.
- Check for exposure history. If your application logs request parameters, review whether tolerance-related endpoints ever received anomalous or SQL-like input historically, particularly if the affected Django version was running for an extended period before patching.
How Safeguard Helps
CVE-2020-9402 is a textbook example of the risk that lives in the long tail of a framework's dependency surface: not the core ORM path everyone tests, but a backend-specific code path (Oracle GIS support) that's easy to overlook until a security researcher finds it. Safeguard is built to catch exactly this kind of gap before it becomes an incident.
- Software Bill of Materials (SBOM) and dependency inventory give you a precise, continuously updated view of which Django version — and which optional components like
django.contrib.gis— are actually running across every service and container, so "are we affected by CVE-2020-9402" becomes a query, not a fire drill. - Continuous vulnerability scanning flags vulnerable Django versions the moment a new CVE is published and correlates it against your real deployed inventory, prioritizing findings using severity, exploitability signals, and CISA KEV status rather than CVSS score alone.
- Configuration-aware risk context helps surface the narrower conditions that matter for CVEs like this one — for example, whether a flagged service actually uses the Oracle backend and GeoDjango, so security and engineering teams can triage based on real exposure instead of chasing every match in a CVE feed.
- Policy-driven remediation workflows let you set SLAs for patching high-severity dependency vulnerabilities, route findings to the owning team automatically, and verify closure once the patched Django version ships to production — closing the loop between "we found it" and "we fixed it everywhere."
- Supply chain provenance tracking helps you confirm that the Django package actually deployed in your build artifacts matches the patched version you intended to ship, catching the lockfile-drift and stale-image scenarios where a CVE quietly persists after the code fix was merged.
Framework-level vulnerabilities like CVE-2020-9402 rarely announce themselves loudly — they sit in a specific backend, a specific configuration, a specific parameter. Safeguard's job is to make sure that specificity doesn't translate into blind spots, so your team can act on real exposure with confidence instead of guessing.