CVE-2026-33050: OS Command Injection in GeoNode Vector Upload Handler
Vulnerability ID: CVE-2026-33050
CVSS Score: 8.8
Published: 2026-11-06
An operating system command injection vulnerability (CWE-78) exists in GeoNode versions prior to 5.1.0. The vulnerability occurs in the vector upload processing pipeline when handling user-supplied geospatial files. By supplying a crafted layer name containing shell metacharacters, an authenticated attacker with basic data upload privileges can execute arbitrary commands on the underlying server or container hosting the GeoNode application and Celery worker processes.
TL;DR
GeoNode prior to 5.1.0 concatenates unsanitized vector layer names into an ogr2ogr command string and executes it via subprocess.Popen with shell=True, allowing authenticated users with upload permissions to achieve remote code execution.
Technical Details
- Vulnerability Type: CWE-78: OS Command Injection
- CVSS v3.1 Score: 8.8 High (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)
- Attack Vector: Network (Authenticated Vector Dataset Upload)
- Affected Component: geonode/upload/handlers/common/vector.py
- Vulnerable Versions: GeoNode < 5.1.0
- Patched Version: GeoNode 5.1.0
- Exploit Status: No known public exploits; patch diff available
- CISA KEV Status: Not Listed
Affected Systems
- GeoNode (< 5.1.0)
-
geonode: < 5.1.0 (Fixed in:
5.1.0)
Code Analysis
Commit: 1408290
Use shlex.quote, shlex.split, and shell=False in vector upload handler to prevent OS command injection
diff --git a/geonode/upload/handlers/common/vector.py b/geonode/upload/handlers/common/vector.py
index d4db7d746ca..d051daa0f3f 100644
--- a/geonode/upload/handlers/common/vector.py
+++ b/geonode/upload/handlers/common/vector.py
@@ -34,6 +34,7 @@
import os
from subprocess import PIPE, Popen
from typing import List, Optional, Tuple
+import shlex
from celery import chord, group
from django.db import transaction
@@ -278,9 +279,10 @@ def create_ogr2ogr_command(files, original_name, ovverwrite_layer, alternate, **
# vrt file is aready created in import_resource and vrt will be auto detected by ogr2ogr
# and also the base_file will work so can be used as alternative for fallback which will also be autodeteced by ogr2ogr.
input_file = files.get("temp_vrt_file") or files.get("base_file")
- options += f'"{input_file}"' + f" -lco FID={DEFAULT_PK_COLUMN_NAME} "
- options += f'-nln {alternate} "{original_name}"'
+ options += f" {shlex.quote(input_file)} -lco FID={DEFAULT_PK_COLUMN_NAME} "
+
+ options += f" -nln {shlex.quote(alternate)} {shlex.quote(original_name)}"
if layer is not None and "Point" not in ogr.GeometryTypeToName(layer.GetGeomType()):
options += " -nlt PROMOTE_TO_MULTI"
@@ -1579,21 +1581,36 @@ def import_with_ogr2ogr(
If the layer should be overwritten, the option is appended dynamically
"""
try:
- ogr_exe = shutil.which("ogr2ogr")
- options = orchestrator.load_handler(handler_module_path).create_ogr2ogr_command(
+ ogr_exe = shutil.which("ogr2ogr") or "ogr2ogr" # Fallback to string if not found
+
+ options_str = orchestrator.load_handler(handler_module_path).create_ogr2ogr_command(
files, original_name, ovverwrite_layer, alternate, execution_id=execution_id
)
- _datastore = settings.DATABASES["datastore"]
+
+ # shlex.split understands that 'quoted strings' are single arguments
+ cmd_list = [ogr_exe] + shlex.split(options_str)
copy_with_dump = ast.literal_eval(os.getenv("OGR2OGR_COPY_WITH_DUMP", "False"))
if copy_with_dump:
- options += f" | PGPASSWORD={_datastore['PASSWORD']} psql -d {_datastore['NAME']} -h {_datastore['HOST']} -p {_datastore.get('PORT', 5432)} -U {_datastore['USER']} -f -"
+ # If using a pipe (ogr2ogr | psql), we must handle it via Python
+ # because shell=False doesn't understand the "|" symbol.
+ _datastore = settings.DATABASES["datastore"]
+ psql_cmd = ["psql", "-d", _datastore["NAME"], "-h", _datastore["HOST"], "-U", _datastore["USER"], "-f", "-"]
- commands = [ogr_exe] + options.split(" ")
- process = Popen(" ".join(commands), stdout=PIPE, stderr=PIPE, shell=True)
- stdout, stderr = process.communicate()
+ env = os.environ.copy()
+ env["PGPASSWORD"] = _datastore["PASSWORD"]
+
+ p1 = Popen(cmd_list, stdout=PIPE, stderr=PIPE)
+ p2 = Popen(psql_cmd, stdin=p1.stdout, stdout=PIPE, stderr=PIPE, env=env)
+
+ p1.stdout.close()
+ stdout, stderr = p2.communicate()
+ else:
+ # Standard execution with shell=False
+ process = Popen(cmd_list, stdout=PIPE, stderr=PIPE, shell=False)
+ stdout, stderr = process.communicate()
if files.get("temp_vrt_file") and os.path.exists(files["temp_vrt_file"]):
os.remove(files["temp_vrt_file"])
Mitigation Strategies
- Upgrade GeoNode to version 5.1.0 or later to eliminate shell evaluation during vector file imports.
- Restrict dataset upload permissions to trusted, vetted accounts and disable open user registration if not required.
- Apply egress network filtering on GeoNode and Celery worker containers to block unauthorized outbound connections.
- Run GeoNode application and Celery worker processes under non-privileged service accounts with read-only root filesystems.
Remediation Steps:
- Update the geonode Python package to version 5.1.0 or newer: pip install --upgrade 'geonode>=5.1.0'.
- If running a legacy branch (3.x, 4.x, 5.0.x) that cannot be upgraded, manually backport commit 1408290c0bb54872924a9b334a516db9758a2180 to geonode/upload/handlers/common/vector.py.
- Restart both the GeoNode WSGI web server (uWSGI/Gunicorn) and all Celery worker processes to ensure the updated handler code is loaded into memory.
- Audit recent upload logs and system process execution history for signs of anomalous shell invocations spawned by Python or Celery.
References
- GitHub Security Advisory: GHSA-9fpx-fr6j-2vh6
- GeoNode Fix Commit 1408290c0bb54872924a9b334a516db9758a2180
- GeoNode 5.1.0 Release Notes
- CVE.org Record: CVE-2026-33050
- GeoNode Official GitHub Repository
Read the full report for CVE-2026-33050 on our website for more details including interactive diagrams and full exploit analysis.
Top comments (0)