Share
## 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

![PoC verdict โ€” VULNERABLE](screenshots/01-poc-run.png)
*Generic Python PoC against an unpatched 11.3.9 lab โ€” `VERDICT: vulnerable` (HTTP 500 + SQLSTATE on metacharacter probe)*

![PoC verdict โ€” PATCHED](screenshots/05-poc-run-patched.png)
*Same PoC against the same lab after applying the upstream `array_values()` fix โ€” `VERDICT: likely_patched` (no anomalies on metachar probes)*

![SQL injection evidence](screenshots/02-sqli-evidence.png)
*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*

![Fix diff](screenshots/03-fix-diff.png)
*The 3-file, 7-line upstream patch*

![Vulnerable code](screenshots/04-vulnerable-code.png)
*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.