## https://sploitus.com/exploit?id=108B5C3B-AD91-501B-9F9D-A7F4DC457879
# SA-CORE-2026-004 โ Lab, PoC, and Post-mortem
**Drupal core SQL injection via Entity Query condition value array keys.**
Disclosed 2026-05-20 17:00 UTC. Highly critical (20/25). PostgreSQL-backed
Drupal installs reachable through JSON:API filters, REST views, or any
custom code that passes user-controlled arrays into `EntityQuery::condition()`.
## Visual evidence

*Generic Python PoC against an unpatched 11.3.9 lab โ `VERDICT: vulnerable` (HTTP 500 + SQLSTATE on metacharacter probe)*

*Same PoC against the same lab after applying the upstream `array_values()` fix โ `VERDICT: likely_patched` (no anomalies on metachar probes)*

*The smoking gun: HTTP 500 response body leaks the full vulnerable SQL with the attacker payload (`_) AND 1=1--`) inside the WHERE clause and the bind array*

*The 3-file, 7-line upstream patch*

*The bug location โ `foreach ($condition['value'] as $key => $value)` where `$key` is attacker-controlled and flows into the SQL placeholder name*
This directory contains a self-contained reproduction lab, a generic Python
detection PoC, and a written explanation of the bug mechanism so anyone can
reproduce the vulnerability against a controlled Drupal install and validate
their patch deployment.
```
sa-core-2026-004-lab/
โโโ README.md โ you are here
โโโ lab/
โ โโโ docker-compose.yml # Drupal 11.3.9 + PostgreSQL 16, ports bound to 127.0.0.1
โ โโโ setup.sh # operator-side: install Drupal, enable JSON:API, seed data
โ โโโ teardown.sh # docker compose down -v
โโโ exploit/
โ โโโ poc.py # Generic Python PoC (--target / --auth / --mode / --json)
โ โโโ observe-pg.sh # tail PG query log filtered for lab markers
โโโ analysis/
โ โโโ bug.md # bug mechanism explained line-by-line
โโโ evidence/
โ โโโ 03-sqli-evidence.txt # captured HTTP 500 + bind-array proof from a vulnerable run
โโโ screenshots/
โโโ 01-poc-run.png # PoC terminal output โ VERDICT: vulnerable
โโโ 02-sqli-evidence.png # the SQL-injection evidence rendered
โโโ 03-fix-diff.png # upstream patch diff
โโโ 04-vulnerable-code.png# the vulnerable PHP source
```
## Quick start
```bash
cd lab
bash setup.sh # ~5 min: pulls images, installs Drupal, enables JSON:API, seeds 5 articles
cd ../exploit
python3 poc.py --mode full
```
The PoC is a **vulnerability detector**, not a complete data-extraction
exploit. The smoking-gun signal is **HTTP 500 with `SQLSTATE[HY093]: Invalid
parameter number: number of bound variables does not match number of tokens`**
in the response body โ that error proves PDO couldn't reconcile the SQL
placeholder name (e.g. `:title_`) with the bind key (e.g. `:title_) AND 1=1--`),
which only happens because the attacker-controlled array key was concatenated
into the SQL string. From there a fully-weaponised exploit would require
careful payload crafting to produce well-formed SQL while still controlling
the WHERE clause; that's out of scope for this detector.
## The bug, in one paragraph
`core/modules/pgsql/src/EntityQuery/Condition.php::translateCondition()` walks
the condition value array with `foreach ($condition['value'] as $key => $value)`
and concatenates `$key` into the SQL placeholder name: `LOWER(:)`.
When the value is associative (string keys), the attacker controls placeholder
name characters, breaking out of the placeholder grammar into raw SQL. JSON:API
parses `filter[name][condition][value][]=v` URL parameters into PHP
associative arrays with the URL key as the array key, providing the
attacker-controlled string-key feed directly into the vulnerable code path.
Fix: `array_values()` strips keys, forcing 0-indexed integer placeholders.
## Affected versions
Per the official advisory:
| Affected range | Fix version |
|----------------|-------------|
| `>= 8.9.0 = 10.5.0 = 10.6.0 = 11.0.0 = 11.2.0 = 11.3.0 ]=...` where
`` is a non-numeric, non-identifier string (contains parens, SQL
keywords, comment markers).
- **Database logs**: `LOWER(:)` fragments in
the WHERE clauses of queries against entity tables. Note that the JSON:API
URL itself appears in `cache_page` / `cache_data` cid lookups regardless
of whether the EntityQuery actually fires โ those cache-log appearances
are NOT proof of exploitation. Look at the actual entity SELECT statement.