
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| Parameter | Default | Meaning |
|---|---|---|
--url URL | required | Host, GeoServer base, or full WFS endpoint |
--mode auto|layers|fields|values | auto | Operating mode to execute |
--layer NAMESPACE:LAYER | – | Restrict auto mode to one feature type; required for fields and values |
--namespace PREFIX | – | Only consider layers with this namespace prefix |
--capabilities-file FILE | – | Use a local GetCapabilities response instead of a download |
--field NAME | repeatable | Property to export in values mode |
--all-properties | off | Export all properties in values mode |
--unique | off | Suppress duplicate field value combinations |
--page-size N | 500 | Features per GetFeature page; range 1 to 5000 |
--sample-size N | 5 | Sample values per layer for the JSON syntax check; range 1 to 100 |
--sleep SECONDS | 0 | Enable time-based verification; allowed are 0 or 1 to 10 seconds |
--candidate-scope auto|id|all | auto | Candidate selection; auto uses ID fields without sleep and all non-geometry fields with sleep |
--timing-result-type auto|hits|results | auto | Query path of the timing check; auto tries hits, then results |
--sleep-threshold RATIO | 0.7 | Required proportion of the sleep time; range 0.5 to 1.0 |
geoserver_sqli_working.py can call the inventory function directly. As soon as --valid-fields is specified, only the inventory is executed and the program then exits.
python3 geoserver_sqli_working.py \
--target https://HOST \
--valid-fields 4 \
--sleep 1 \
--valid-diagnose output.txt
Internally, the following parameters are passed to wfs_inventory.py:
--target -> --url
--valid-fields -> --valid-fields
--valid-diagnose -> --valid-diagnose
--sleep -> --sleep
--candidate-scope -> --candidate-scope
--timing-result-type -> --timing-result-type
--sleep-threshold -> --sleep-threshold
--confirm-sleep -> --confirm-sleep
Without an explicit --sleep, the time-based check remains disabled in inventory mode.
Only for explicitly authorized test systems:
python3 geoserver_sqli_working.py \
--target https://HOST/geoserver/wfs \
--typename namespace:layer \
--field_name FIELD \
--sleep 5
In PoC mode, the target path is not automatically normalized. Here, the full WFS endpoint should be specified.
The mode first performs a baseline/sleep test, then tests the available time-based oracle variants, and upon successful confirmation reads server/database metadata by default. With --query, a custom scalar query can be specified instead.
The PoC mode currently disables TLS certificate verification internally. For a pure inventory,
wfs_inventory.pyshould preferably be used, since TLS is verified by default there.
geoserver_sqli_working.py| Parameter | Default | Meaning |
|---|---|---|
--target URL | required | Target host or WFS endpoint; use full WFS path in PoC mode |
--typename NAME | fink_bku:fink_meta_mitte_suedwest | Feature type for PoC mode |
--field_name NAME | required in PoC mode | Simple XSD field name for jsonArrayContains |
--field-name NAME | alias | Alias for --field_name |
--valid-fields N | – | Enable inventory mode and stop after N valid layers |
--valid-diagnose FILE | – | Write verbose inventory report to FILE; requires --valid-fields |
--sleep SECONDS | PoC: 5, inventory: off | Sleep duration of the respective mode |
--candidate-scope auto|id|all | auto | Candidate selection in inventory mode |
--timing-result-type auto|hits|results | auto | Timing query path in inventory mode |
--sleep-threshold RATIO | 0.7 | Timing threshold in inventory mode |
--confirm-sleep SECONDS | automatic | Confirmation sleep in inventory mode |
--query SQL | – | Custom scalar query in PoC mode |
--debug | off | Display conditions, runtimes, and decisions in PoC mode |
The auto mode can generate JSON Lines instead of text blocks:
python3 wfs_inventory.py \
--url https://HOST \
--valid-fields 10 \
--report-format jsonl \
--output report.jsonl
With --diagnose, each record additionally contains URLs, candidate metadata, check values, and field-specific GetFeature URLs.
Use a proxy:
python3 wfs_inventory.py \
--url https://HOST \
--proxy http://127.0.0.1:8080 \
--valid-fields 4
Trust a custom proxy CA:
python3 wfs_inventory.py \
--url https://HOST \
--proxy http://127.0.0.1:8080 \
--proxy-ca proxy-ca.pem \
--valid-fields 4
Disable TLS verification for an authorized test system:
python3 wfs_inventory.py \
--url https://HOST \
--insecure \
--valid-fields 4
--proxy-ca and --insecure cannot be used together.
For large GetCapabilities responses, the following options help:
python3 wfs_inventory.py \
--url https://HOST \
--valid-fields 4 \
--max-layers 100 \
--sample-size 1 \
--timeout 10 \
--retries 0 \
--delay 0
Notes:
GetCapabilities is requested only once and then streamed.--valid-fields 4 ends the scan only after four valid layers. If fewer hits exist, the search runs to the end or until --max-layers.--sample-size 1 reduces the effort of the JSON heuristic.--timing-result-type auto, after a negative hits result, two additional results requests are executed.--candidate-scope id reduces the number of timing requests but can miss vulnerable fields with other names.--namespace and --start-layer-index can further limit the search space.The read operation timed out--timeout if the server responds slowly.--retries 0 to avoid long retries.--max-layers and --namespace.--sample-size 1 for the JSON heuristic.--sleep, candidates must pass the XSD/name check and the JSON syntax check depending on --candidate-scope.--sleep, only time-confirmed candidates are output as a parameter block; negative measurements appear as [sleep-check] on stderr.--max-layers.Cannot do natural order without a primary keyFor the first GetFeature page, the script does not send startIndex=0, since some GeoServer/JDBC layers without a primary key already force a natural sort order this way. When exporting further pages, such a layer may still require a primary key or a server-side supported sort order.
schema does not define ...TypeThe script considers both the usual <LayerName>Type and a deviating complexType referenced in the global XSD layer element or an anonymous one. If the error persists, the relevant DescribeFeatureType response should be examined with --diagnose.
The full OWS error message is output to stderr. Common causes are unsupported properties, server-side paging defaults, or a layer-specific data source configuration.
python3 wfs_inventory.py --help
python3 geoserver_sqli_working.py --help
--confirm-sleep SECONDS | 0/automatic | Confirmation sleep; 0 uses at least 3× the first sleep time, range 1 to 10 |
--start-layer-index N | 0 | Skip the first N streamed layers |
--max-layers N | 0 | Process at most N layers; 0 means unlimited |
--valid-fields N | 0 | Stop after N valid layer blocks; 0 means unlimited |
--valid N | alias | Backward-compatible alias for --valid-fields |
--max-features N | 0 | Stop after N features in values mode; 0 means unlimited |
--format jsonl|csv|text | jsonl | Output format in values mode |
--output FILE | stdout | Write report or values to a file |
--report-format blocks|jsonl | blocks | Format of the auto report |
--diagnose | off | Add URLs, types, selection flags, and check statistics |
--valid-diagnose FILE | – | Enable diagnostics and write directly to FILE |
--delay SECONDS | 0.1 | Pause between result/check steps |
--timeout SECONDS | 30 | Timeout per HTTP request |
--retries N | 2 | Retries after timeout or network error; range 0 to 10 |
--proxy URL | – | HTTP(S) proxy, for example http://127.0.0.1:8080 |
--proxy-ca FILE | – | PEM CA certificate to trust a proxy certificate |
--insecure | off | Disable TLS certificate verification |
--authorization TEXT | I_AM_AUTHORIZED | Security confirmation; must be exactly I_AM_AUTHORIZED |