Safeguard
Vulnerability Analysis

Django GIS SQL injection (CVE-2020-9402)

CVE-2020-9402 lets attackers inject SQL via Django GIS's tolerance parameter on Oracle. Versions, CVSS/EPSS data, timeline, and fixes inside.

Nayan Dey
Security Researcher
7 min read

Django's GeoDjango stack quietly shipped a critical SQL injection flaw for over a year before anyone noticed: CVE-2020-9402 allowed attackers to inject arbitrary SQL through the tolerance parameter used by GIS functions and aggregates when Django's ORM targets an Oracle database. If an application passed untrusted input into that parameter — a common pattern in geospatial search, "find nearby" features, or map-based filtering — an attacker could bypass Django's normal query escaping and manipulate the underlying SQL directly, putting data confidentiality, integrity, and availability all at risk.

This wasn't a theoretical edge case. GeoDjango (django.contrib.gis) is widely used in logistics, delivery, real estate, and mapping applications, and Oracle Spatial remains a common backend in enterprise deployments of exactly that kind of software. Any codebase that exposed a distance, buffer, or line-merging query to user-controlled tolerance values was directly exposed.

What Was Vulnerable

The root cause sits in how Django's ORM builds SQL for Oracle's spatial functions. Oracle's SDO_GEOM package doesn't support bind parameters the same way standard column values do for certain arguments, so Django's GIS backend fell back to string-formatting the tolerance value directly into the generated SQL rather than passing it as a properly escaped parameter. That gap affected two categories of GIS operations:

  • Lookups that accept a tolerance argument, such as distance_lte, distance_gte, dwithin, and related spatial comparison lookups.
  • Aggregates and functions including Union, MakeLine, and other GIS aggregates that accept a tolerance keyword argument.

If application code constructed any of these with a tolerance value sourced from a query parameter, form field, or API payload — even indirectly — the resulting SQL string could be broken out of and rewritten by an attacker. This is a classic case of a "safe by default" ORM having one narrow, backend-specific code path that quietly opted out of parameterized SQL.

Critically, this issue was Oracle-specific. Django's PostgreSQL, MySQL, and SQLite GIS backends were not affected because they handle the equivalent spatial function calls differently. That narrows the blast radius considerably, but it does nothing to help teams who standardized on Oracle Spatial for enterprise GIS workloads — and it's exactly the kind of "affects only backend X" nuance that generic dependency scanners routinely miss when they flag a package version without checking whether the vulnerable code path is even reachable.

Affected Versions and Components

CVE-2020-9402 affects the following Django release lines:

  • Django 1.11, from 1.11 up to and including 1.11.28
  • Django 2.2, from 2.2 up to and including 2.2.10
  • Django 3.0, from 3.0 up to and including 3.0.3

The affected component is django.contrib.gis (GeoDjango) specifically when configured against an Oracle database backend. The vulnerability class is CWE-89 (Improper Neutralization of Special Elements used in an SQL Command), the textbook definition of SQL injection.

Fixed versions are:

  • Django 1.11.29
  • Django 2.2.11
  • Django 3.0.4

Any application still pinned below these versions and running GeoDjango against Oracle should be treated as exposed until patched, regardless of how the tolerance value is sourced in application code — defense-in-depth arguments aside, the safest assumption is that any code path can eventually be reached by user input.

CVSS, EPSS, and KEV Context

  • CVSS v3.1 Base Score: 8.8 (High) — vector AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H. The score reflects network-exploitability with low attack complexity and no user interaction, offset only by the requirement for low-privilege access to the affected application. Confidentiality, integrity, and availability impacts are all rated high, consistent with SQL injection's potential to read, modify, or destroy data at the database layer.
  • EPSS: approximately 0.225 (22.5% predicted exploitation probability in the next 30 days), placing it around the 97th percentile of all scored CVEs — notably high real-world exploitation interest for a five-year-old, backend-specific vulnerability, and a reminder that "old" doesn't mean "cold" once a CVE and its details are public and searchable.
  • CISA KEV: CVE-2020-9402 is not currently listed in CISA's Known Exploited Vulnerabilities catalog. Absence from KEV should not be read as absence of risk — KEV tracks confirmed active exploitation, not exploitability, and plenty of high-EPSS vulnerabilities never make the list simply because telemetry on exploitation in the wild is incomplete.

Together, these signals describe a vulnerability with a serious theoretical and demonstrated severity ceiling, real attacker interest as measured by EPSS, but no confirmed mass-exploitation campaign — the profile of a vulnerability that matters most to the specific slice of organizations running the affected configuration, rather than to the internet at large.

Timeline

  • Discovery: The vulnerability was identified and reported by Norbert Szetei of Doyensec through Django's responsible disclosure process.
  • March 4, 2020: The Django Software Foundation published a coordinated security release addressing CVE-2020-9402, shipping patched versions 1.11.29, 2.2.11, and 3.0.4 simultaneously across all three supported release branches.
  • March 5, 2020: CVE-2020-9402 was published in the National Vulnerability Database with an initial CVSS assessment.
  • Following weeks: Downstream distributions issued their own advisories and backported patches, including Debian, Ubuntu (USN-4296-1), Fedora, and Gentoo, reflecting how widely GeoDjango and Django itself are packaged and redistributed.
  • Ongoing: The CVE continues to appear in dependency and SCA scan results for any codebase still pinned to a pre-patch Django release, and its elevated EPSS score suggests it remains on the radar of automated exploitation tooling years after disclosure.

Remediation Steps

  1. Upgrade Django immediately to 1.11.29, 2.2.11, 3.0.4, or any later release in a supported line. This is a patched-library fix, not a configuration workaround — there is no supported way to disable the vulnerable code path while staying on an affected version.
  2. Confirm your database backend. If you are not running django.contrib.gis against Oracle, your direct exposure to this specific CVE is lower, but you should still upgrade as a matter of hygiene, since the same Django release lines carry other fixes.
  3. Audit GIS query construction. Search your codebase for uses of distance_lte, distance_gte, dwithin, Union, MakeLine, and other tolerance-accepting GIS lookups and aggregates. Confirm that tolerance values are never derived from unvalidated user input, and add explicit type/range validation (e.g., casting to float within a bounded range) as a defense-in-depth measure independent of the framework patch.
  4. Regenerate and re-audit your SBOM after upgrading. Confirm the new Django version is reflected everywhere the dependency is declared — including transitive pins in requirements files, lockfiles, container base images, and any vendored copies.
  5. Check downstream packages built on GeoDjango (custom GIS libraries, internal geocoding services, third-party mapping add-ons) for their own pinned Django version constraints, since an outdated transitive constraint can silently block the upgrade or reintroduce the vulnerable version in a rebuild.
  6. Review database logs and WAF rules retroactively for anomalous SQL patterns in requests that touch geospatial endpoints, particularly if the application has been internet-facing since before March 2020 and hasn't been continuously patched.
  7. Add regression tests around GIS query construction to ensure future refactors don't reintroduce raw string interpolation into tolerance or similar parameters — a common failure mode when performance-tuning spatial queries by hand.

How Safeguard Helps

Generic SCA tools would flag every Oracle-backed and non-Oracle-backed Django app alike for CVE-2020-9402, forcing security teams to manually triage which deployments actually use django.contrib.gis against Oracle and which are unaffected noise. Safeguard's reachability analysis instead traces whether your application actually invokes the vulnerable GIS lookups and aggregates against an Oracle backend, so remediation effort goes to real exposure first. Griffin AI, Safeguard's autonomous security agent, can correlate that reachability signal with your dependency graph and CVSS/EPSS context to prioritize this finding correctly against the rest of your backlog, rather than treating every "Django < 3.0.4" match as equally urgent. Continuous SBOM generation and ingestion keeps every Django version — down to transitive GeoDjango and geospatial library pins — visible across your services, so a stale pin in a forgotten container image doesn't quietly reintroduce this exposure. And when action is warranted, Safeguard can open an auto-fix pull request that bumps Django to a patched release and runs your test suite, turning a multi-team advisory review into a single reviewable diff.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.