
Python scripts for inventorying GeoServer WFS endpoints and verifying time-based SQL injection vulnerabilities in PostGIS/GeoTools, with a dedicated PoC mode for authorized testing.
This repository contains two Python scripts for investigating GeoServer WFS endpoints.
wfs_inventory.pyPurpose: Inventory layers, XSD fields, and WFS values, and optionally verify candidates via a time-based check.
Execution examples:
python3 wfs_inventory.py --url https://HOST --valid-fields 4
python3 wfs_inventory.py --url https://HOST --valid-fields 4 --sleep 1 --confirm-sleep 5 --candidate-scope auto --timing-result-type auto
python3 wfs_inventory.py --url https://HOST --layer namespace:layer --sleep 1 --confirm-sleep 5 --valid-diagnose output.txt
geoserver_sqli_working.pyPurpose: Combined entry point for inventory as well as a separate PoC mode.
Execution examples:
python3 geoserver_sqli_working.py --target https://HOST --valid-fields 4 --sleep 1 --confirm-sleep 5
python3 geoserver_sqli_working.py --target https://HOST/geoserver/wfs --typename namespace:layer --field_name FIELD --sleep 5
python3 geoserver_sqli_working.py --target https://HOST/geoserver/wfs --typename namespace:layer --field_name FIELD --sleep 5 --query "SELECT current_database()"
Important note: The time-based check and the PoC mode may only be used against systems for which explicit testing authorization has been granted. The normal inventory mode uses exclusively regular WFS operations.
Optionally make executable:
chmod +x wfs_inventory.py geoserver_sqli_working.py
python3 wfs_inventory.py \
--url https://HOST \
--valid-fields 4
The default path /geoserver/wfs is appended automatically. The following specifications are therefore equivalent:
https://HOST
https://HOST/geoserver
https://HOST/geoserver/wfs
For a non-standard installation, the full WFS path must be specified.
Only for explicitly authorized systems:
python3 wfs_inventory.py \
--url https://HOST \
--valid-fields 4 \
--sleep 1
During the check, a measurement per candidate appears on stderr:
[sleep-check] phase=screen typeName=namespace:layer field_name=FIELD resultType=hits baseline=0.120s test=1.128s delta=1.008s passed=true
[sleep-check] phase=confirm typeName=namespace:layer field_name=FIELD resultType=hits requested=3s baseline=0.118s test=3.125s delta=3.007s vulnerable=true
A passed screen is not yet a positive finding. Only when the second, longer measurement also passes is a parameter block output.
python3 wfs_inventory.py \
--url https://HOST \
--valid-fields 4 \
--sleep 1 \
--valid-diagnose output.txt
--valid-diagnose FILE automatically enables verbose diagnostics and writes them to the specified file.
The default auto mode processes the WFS information in this order:
GetCapabilities is opened once:
/geoserver/wfs?service=WFS&acceptVersions=2.0.0&request=GetCapabilities
The XML response is streamed. As soon as a FeatureType/Name is found, the next typeName is determined.
For this layer, DescribeFeatureType is immediately executed with version=2.0.0.
The script extracts the XSD elements and selects string/JSON fields with ID- or number-like names.
The candidate is checked depending on the invocation:
--sleep: Sample values must be syntactically valid JSON.--sleep N: A control request and a time-based test request are measured. The JSON value check is skipped in this case.The result is output immediately and flushed.
Only then is the next typeName read from the ongoing GetCapabilities response.
This means a large GetCapabilities response does not need to be fully processed before the first result appears.
Without --sleep, the automatic mode by default considers string/JSON fields with ID- or number-like names.
With --sleep, --candidate-scope auto instead uses all simple non-geometry fields. XSD type and ID name patterns no longer block the timing check. This prevents false negatives for numeric, date/boolean, or vendor-specific XSD types.
Recognized name patterns include, among others:
id
*_id
*_fid
nr_*
*_nr
*nummer*
fid
uuid
guid
key
objectid
For the time-based check, the field name must additionally be a simple identifier in the format [A-Za-z_][A-Za-z0-9_]*.
Without --sleep, it is checked whether the observed values can be syntactically interpreted as JSON. Therefore, for example, the string "383205" also counts as a candidate because its content represents a valid JSON number. This check is a heuristic and not proof of vulnerability.
With --sleep 1, only the time measurement decides. A candidate is considered positive by default if the test request takes at least 70 percent of the requested sleep time in addition to the control request. --timing-result-type auto first checks resultType=hits and, in the event of a negative result, subsequently resultType=results.
Provisionally positive measurements are mandatorily confirmed with a longer sleep time. Without an explicit --confirm-sleep, the script uses max(3, --sleep * 3), capped at 10 seconds. This means a single latency spike with --sleep 1 no longer leads to vulnerable=true.
The standard output contains one block per valid layer:
typeName=namespace:layer
field_name1=FIELD_A
parameter_string1=https://HOST/geoserver/wfs --typename namespace:layer --field_name FIELD_A
field_name2=FIELD_B
parameter_string2=https://HOST/geoserver/wfs --typename namespace:layer --field_name FIELD_B
parameter_stringN contains the normalized WFS endpoint and the matching values for --typename and --field_name.
--valid-fields N counts layer blocks, not individual fields. If fewer than N valid layers exist, the remaining catalog continues to be examined. --max-layers can be used to limit the maximum runtime.
The options --diagnose or --valid-diagnose FILE additionally include:
GetCapabilities URL,DescribeFeatureType URL,GetFeature URL andExample:
python3 wfs_inventory.py \
--url https://HOST \
--valid-fields 10 \
--valid-diagnose diagnose.txt
python3 wfs_inventory.py \
--url https://HOST \
--mode layers
Restrict to a namespace:
python3 wfs_inventory.py \
--url https://HOST \
--mode layers \
--namespace fink
python3 wfs_inventory.py \
--url https://HOST \
--mode fields \
--layer namespace:layer
Each field is output as a JSON object with name, XSD type, nillable, id_candidate, and usable_property.
python3 wfs_inventory.py \
--url https://HOST \
--mode values \
--layer namespace:layer \
--field FIELD_A \
--field FIELD_B \
--max-features 100 \
--format jsonl \
--output values.jsonl
Export all properties:
python3 wfs_inventory.py \
--url https://HOST \
--mode values \
--layer namespace:layer \
--all-properties \
--max-features 100
With --unique, identical combinations of selected field values are output only once.
wfs_inventory.py