Human
Machine
REPRO-2026-00133 CRITICAL RCE
Verified
Drupal core: unauthenticated SQL injection via JSON:API filter array keys
drupal/core (composer) May 22, 2026
What's the vulnerability?
A vulnerability in Drupal core's Entity Query SQL backend allows an unauthenticated (anonymous) attacker to inject arbitrary SQL into queries on sites backed by a PostgreSQL database. Per the advisory this can lead to information disclosure and, in some configurations, privilege escalation or remote code execution. The issue does not trigger on MySQL/MariaDB or SQLite.
Root Cause Analysis
# RCA Report: CVE-2026-9082 — Drupal Core SQL Injection (SA-CORE-2026-004)
## Summary
CVE-2026-9082 is a highly critical SQL injection vulnerability in Drupal core's Entity Query SQL backend that affects only PostgreSQL-backed sites. The vulnerability exists because attacker-controlled array keys from entity query condition values are directly concatenated into named SQL placeholder identifiers in the PostgreSQL-specific `Condition::translateCondition()` method. An unauthenticated attacker can inject arbitrary characters (such as backticks, quotes, or SQL fragments) into the raw SQL text by providing malicious keys through JSON:API collection filter query parameters or the JSON login endpoint, resulting in SQL syntax errors or — with more sophisticated payloads — information disclosure and potential privilege escalation.
## Impact
- **Package/component affected**: Drupal core, specifically the PostgreSQL entity query condition handler (`core/modules/pgsql/src/EntityQuery/Condition.php`) and upstream condition compilation in `core/lib/Drupal/Core/Entity/Query/Sql/Condition.php` and `ConditionAggregate.php`
- **Affected versions**: Drupal 8.9.0 through <10.4.10, 10.5.0 through <10.5.10, 10.6.0 through <10.6.9, 11.0.0 through <11.1.10, 11.2.0 through <11.2.12, 11.3.0 through <11.3.10
- **Risk level**: Highly critical (20/25 on Drupal's scale; CVSS 3.1 base 6.5)
- **Consequences**: Unauthenticated SQL injection on PostgreSQL sites. Can lead to information disclosure, privilege escalation, and in some configurations remote code execution. MySQL/MariaDB and SQLite backends are **not** affected.
## Root Cause
The root cause is a trust boundary violation in `core/modules/pgsql/src/EntityQuery/Condition.php`. When processing an `IN` condition with case-insensitive comparison on PostgreSQL, the code iterates over `$condition['value']` and appends each array key directly into the SQL placeholder name:
```php
foreach ($condition['value'] as $key => $value) {
$where_id = $where_prefix . $key;
$condition['where'] .= 'LOWER(:' . $where_id . '),';
...
}
```
When JSON:API filter parameters or JSON login bodies decode JSON objects into associative PHP arrays, the attacker controls both the keys and values. A malicious key like `` ` `` (backtick) becomes part of the placeholder name: `LOWER(:node_field_data_title`)`. PDO then cannot bind `:node_field_data_title`` because it was never added to the bound arguments list, producing `SQLSTATE[HY093]: Invalid parameter number`.
More advanced payloads can inject actual SQL fragments (e.g., using `)` to close the `LOWER()` function call and `||` for PostgreSQL string concatenation), enabling boolean-blind SQL injection or error-based data exfiltration.
The fix is three `array_values()` calls that strip attacker-controlled keys before the vulnerable loop is reached:
- `core/lib/Drupal/Core/Entity/Query/Sql/Condition.php`
- `core/lib/Drupal/Core/Entity/Query/Sql/ConditionAggregate.php`
- `core/modules/pgsql/src/EntityQuery/Condition.php`
## Reproduction Steps
1. **Reference script**: `repro/reproduction_steps.sh`
2. **What the script does**:
- Ensures PostgreSQL is running and creates a fresh `drupal` database with user `drupal`/`drupal`
- Prepares the Drupal core repository (clones if needed, installs Composer dependencies)
- **Vulnerable phase**: Checks out Drupal 11.3.9, installs a minimal site with PostgreSQL, enables the JSON:API module, creates a `page` content type and node, starts PHP's built-in server, and sends an anonymous GET request to `/jsonapi/node/page?filter[t][condition][path]=title&filter[t][condition][value][%60]=x`
- **Fixed phase**: Repeats the same setup on Drupal 11.3.10 and sends the identical exploit request
3. **Expected evidence**:
- **Vulnerable 11.3.9**: The JSON:API endpoint returns HTTP 500 with a `SQLSTATE[HY093]: Invalid parameter number` error. The response body contains the malformed SQL with the injected backtick inside the placeholder name (`:node_field_data_title``).
- **Fixed 11.3.10**: The same request returns HTTP 200 with a normal JSON:API response body (`data: []`), confirming the injection is blocked.
## Evidence
### Log files
- `logs/vulnerable_response.txt` — HTTP response from the exploit against 11.3.9
- `logs/fixed_response.txt` — HTTP response from the same exploit against 11.3.10
- `logs/repro_output.txt` — Full console output from the first reproduction run
- `logs/repro_output_2.txt` — Full console output from the second (idempotency) run
### Key excerpts
**Vulnerable 11.3.9** (`logs/vulnerable_response.txt`):
```
HTTP_CODE:500
{"errors":[{"title":"Internal Server Error","status":"500","detail":"SQLSTATE[HY093]: Invalid parameter number: parameter was not defined: SELECT ... WHERE ((LOWER(\"node_field_data\".\"title\") = (LOWER(:node_field_data_title`)))) ..."}]}
```
**Fixed 11.3.10** (`logs/fixed_response.txt`):
```
HTTP_CODE:200
{"jsonapi":{"version":"1.1",...},"data":[],"links":{"self":{"href":"http://localhost:8080/jsonapi/node/page?..."}}}
```
### Environment details
- **PHP**: 8.4.19 (CLI + built-in server)
- **PostgreSQL**: 16.13
- **OS**: Ubuntu 24.04
- **Drupal test profile**: Minimal install profile with JSON:API module enabled
## Recommendations / Next Steps
1. **Immediate upgrade**: All Drupal sites running on PostgreSQL should upgrade to the patched releases: 10.4.10, 10.5.10, 10.6.9, 11.1.10, 11.2.12, or 11.3.10 (whichever matches their branch).
2. **Verify backend**: Site operators should confirm their database backend. Sites on MySQL, MariaDB, or SQLite are not affected by this specific vulnerability.
3. **WAF tuning**: If immediate patching is not possible, consider blocking or sanitizing JSON:API filter query strings that contain special characters (`
`, `'`, `"`, `)`, `|`, `--`) in filter value keys, as these are the characters that reach the vulnerable placeholder name construction.
4. **Regression testing**: After upgrading, verify JSON:API filter functionality still works correctly for normal use cases, since the fix reindexes condition value arrays with `array_values()`.
## Additional Notes
- **Idempotency confirmed**: `repro/reproduction_steps.sh` was executed twice consecutively with identical results (HTTP 500 + HY093 on 11.3.9, HTTP 200 on 11.3.10).
- **Limitations**: The reproduction requires a real running Drupal site over HTTP with a PostgreSQL database. A minimal PHP harness or mock objects would not satisfy the ticket requirements and were explicitly rejected.
- **Edge cases**: The vulnerability only triggers on the PostgreSQL-specific case-insensitive `IN` condition path. MySQL/MariaDB sites are unaffected because they use a different condition translation path that does not wrap comparisons in `LOWER()`.
One Command
Verify with pruva-verify
Run the Pruva CLI to automatically fetch and execute the reproduction script.
pruva-verify REPRO-2026-00133 or
pruva-verify CVE-2026-9082 Install:
curl -fsSL https://pruva.dev/install.sh | sh Or Run Manually
1
Download the script
curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00133/artifacts/reproduction_steps.sh 2
Make executable
chmod +x reproduction_steps.sh 3
Run the script
./reproduction_steps.sh Run in a VM, container, or disposable environment. This exploits a real vulnerability.
How Pruva Reproduced This
Watch the AI agent's step-by-step process.
Loading session...
Artifacts
No artifacts available