## Ticket: CVE-2026-9082 — Drupal core SQL injection (SA-CORE-2026-004)

**Advisory**: SA-CORE-2026-004 — https://www.drupal.org/sa-core-2026-004
**CVE**: CVE-2026-9082 | **CWE-89** (SQL Injection) | **CAPEC-66**
**Severity**: Drupal "Highly critical" (20/25); NVD CVSS 3.1 base 6.5 (AV:N/AC:L/PR:N/UI:N)
**Repository**: https://git.drupalcode.org/project/drupal

### Impact

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.

### Affected / fixed versions

Affected: `8.9.0` → `<10.4.10`, `10.5.0` → `<10.5.10`, `10.6.0` → `<10.6.9`,
`11.0.0` → `<11.1.10`, `11.2.0` → `<11.2.12`, `11.3.0` → `<11.3.10`.

Fixed: `10.4.10`, `10.5.10`, `10.6.9`, `11.1.10`, `11.2.12`, `11.3.10`.

Reproduce on a vulnerable build (recommended: **11.3.9**) and verify the fix on
**11.3.10**.

### Where to look (from the public security release)

The fix ships in release 11.3.10. Compare it against 11.3.9:

```bash
git clone https://git.drupalcode.org/project/drupal.git
cd drupal && git diff 11.3.9 11.3.10
```

The security-relevant code changes are confined to the entity query SQL
condition translation:

- `core/modules/pgsql/src/EntityQuery/Condition.php`
- `core/lib/Drupal/Core/Entity/Query/Sql/Condition.php`
- `core/lib/Drupal/Core/Entity/Query/Sql/ConditionAggregate.php`

Read that diff and derive the root cause before writing any exploit. The branch
that builds raw SQL from an entity-query condition whose `value` is an array is
the area of interest — pay attention to how named SQL placeholders are
constructed from that array.

### Environment

- This run uses the **local** execution backend — **no Docker**. Install
  everything natively on the host (you have root).
- Required: **PostgreSQL** (`apt-get install postgresql`), **PHP 8.3** with the
  `pgsql`, `gd`, `xml`, `mbstring`, `curl`, and `zip` extensions, and
  **Composer**. The vulnerability is PostgreSQL-specific — do not use
  MySQL/MariaDB or SQLite.
- Install Drupal 11.3.9: `composer create-project drupal/recommended-project`
  with `drupal/core-*` pinned to `11.3.9`, or clone the core git repo. The PHP
  built-in server (`php -S 127.0.0.1:8888`) is sufficient — no Apache/nginx
  needed. Run the site installer against the PostgreSQL database.
- Enable the `jsonapi` core module — JSON:API collection `filter` parameters are
  the primary anonymous-reachable surface that turns request input into entity
  query conditions.

### Reproduction goals — MANDATORY: live runtime exploitation

This reproduction MUST exploit the vulnerability through a **running Drupal
site serving real HTTP requests**. The following is **explicitly NOT acceptable**
and will be rejected:

- A PHP harness that `require`s `Condition.php` and calls `translateCondition()`
  (or `compile()`) directly.
- Any script that uses a mock/stub `Select` object or reimplements Drupal logic.
- Static analysis, `grep`, or unit-test-style checks.

The proof must be the vulnerability triggering inside the live application's
request-handling path, exactly as a remote attacker would reach it.

1. Stand up Drupal 11.3.9 on PostgreSQL: `composer create-project`, run the site
   installer, serve it with `php -S 127.0.0.1:8888`, and enable the `jsonapi`
   module. Confirm the site answers HTTP requests (e.g. `GET /jsonapi`).
2. As an **anonymous, unauthenticated HTTP client** (`curl`), send a request to
   a JSON:API collection endpoint whose `filter` parameter is structured so the
   condition `value` is an associative array with attacker-controlled **keys**
   (the keys become SQL placeholder names in `pgsql/EntityQuery/Condition.php`).
3. Demonstrate the injection in the live request: capture concrete runtime proof
   from the HTTP response or the PostgreSQL query log — a forced SQL error that
   proves injection, a time-based delay via `pg_sleep`, or data returned outside
   the legitimate filter scope. The generated SQL must come from the running
   server, not a harness.
4. Repeat the **identical HTTP request** against a Drupal **11.3.10** site and
   confirm it is now safely parameterized (no injection, no error/delay).
5. If the live JSON:API path genuinely cannot be reached, the run must report
   `BLOCKED` with the specific obstacle — it must NOT fall back to a harness.

### Expected artifacts

- `repro/reproduction_steps.sh` — runnable end-to-end: installs Drupal on
  PostgreSQL, starts the HTTP server, sends the attacking HTTP request, and
  verifies 11.3.9 vs 11.3.10.
- `repro/rca_report.md` — root cause analysis derived from the patch diff.
- `repro/patch_analysis.md` — confirmation of the fix (vulnerable 11.3.9 vs
  patched 11.3.10).
- Captured evidence: the full HTTP request/response transcript and/or the
  PostgreSQL statement log showing the injected SQL on 11.3.9 and the same
  request safely handled on 11.3.10.
