ekofyi
Security Research10 min read

Drupal's Database Abstraction Layer Just Broke: SQL Injection via PostgreSQL in Core

A critical SQL injection vulnerability in Drupal core's database abstraction API affects all PostgreSQL-backed sites. Here's how it works, how to detect it, and what to do right now.

The One Layer You Trusted Just Failed You

Imagine you're running a Drupal site backed by PostgreSQL. You've done the right things — kept modules updated, run security audits, maybe even reviewed custom code for injection flaws. But you never questioned the database abstraction layer itself. Why would you? That's the framework's job. It's supposed to sanitize everything before it touches the database.

Except now it doesn't. Drupal just published SA-CORE-2026-004, a highly critical SQL injection vulnerability that lives inside Drupal core's own database API — the very layer designed to prevent this exact class of attack. If your site uses PostgreSQL as its backend, you're exposed.

This isn't some obscure contrib module with twelve installs. This is core. The database abstraction layer that every single query flows through. When that breaks, the entire security model of the application collapses underneath you.

The advisory dropped on drupal.org and it's rated highly critical — one step below the maximum severity Drupal assigns. For SQL injection in a framework's own sanitization layer, that tracks. Let's break down exactly what's happening, why PostgreSQL specifically is affected, and what you need to do about it today.

What Happened

Drupal's database abstraction API (built on top of PDO) is the gatekeeper between your application logic and raw SQL execution. Every db_query(), every entity query, every Views-generated SQL statement passes through this layer. Its entire purpose is to parameterize inputs and prevent injection — it's the reason Drupal developers don't write manual escaping logic.

SA-CORE-2026-004 reveals that this abstraction layer has a flaw in how it handles certain inputs when the backend database is PostgreSQL. An attacker can send specially crafted requests that bypass the sanitization logic, resulting in arbitrary SQL injection.

The key detail here: this only affects PostgreSQL backends. MySQL and MariaDB installations appear unaffected by this specific vector. That's not because MySQL is more secure — it's because the vulnerability lies in how Drupal's abstraction layer translates certain operations for PostgreSQL's SQL dialect specifically.

The impact is severe. Successful exploitation leads to information disclosure at minimum — dumping user tables, session tokens, password hashes, configuration secrets. In some cases, it can escalate to privilege escalation, allowing an attacker to grant themselves admin access or modify content. Depending on PostgreSQL configuration and permissions, it could potentially go further.

This is an unauthenticated attack vector. The advisory indicates that specially crafted requests trigger it — meaning no login is required. That's the worst-case scenario for any injection vulnerability.

Technical Deep-Dive: How the Abstraction Layer Breaks

To understand why this happens, you need to understand how Drupal's database layer works. When you write a query using Drupal's API, it looks something like this:

Here's a typical safe query using Drupal's abstraction layer:

php
$result = \Drupal::database()->query(
  'SELECT name, mail FROM {users_field_data} WHERE uid = :uid',
  [':uid' => $user_input]
);

The :uid placeholder gets parameterized by PDO, and the value is bound separately from the SQL statement. This is textbook prepared statement usage, and under normal circumstances, it's safe.

But Drupal's abstraction layer does more than simple placeholder binding. It handles complex operations — IN clauses with arrays, LIKE operations, table prefix expansion, and database-specific SQL dialect translation. The vulnerability lives somewhere in this translation layer for PostgreSQL.

Consider how Drupal handles array expansion for IN clauses. The API allows you to pass arrays as placeholder values:

php
// Drupal expands this into multiple placeholders internally
$result = \Drupal::database()->query(
  'SELECT name FROM {users_field_data} WHERE uid IN (:uids[])',
  [':uids[]' => $user_provided_array]
);

Internally, Drupal's expandArguments() method iterates over the array and generates individual placeholders like :uids_0, :uids_1, etc. The vulnerability likely exists in how these expanded placeholders or their associated values are handled in the PostgreSQL driver's query preparation.

The root cause appears to be insufficient sanitization of placeholder names or values during the expansion/translation phase specific to PostgreSQL. Here's a conceptual illustration of the vulnerable pattern:

php
// Conceptual vulnerable pattern in Drupal's PG driver
// The placeholder key itself may not be properly sanitized
foreach ($args as $key => $value) {
  // If $key contains crafted SQL and isn't validated...
  $new_key = $key . '_' . $index;
  // This constructed key gets interpolated into the query string
  $query = str_replace($original_placeholder, $new_key, $query);
}

The critical security principle violated here is never trust the structure of input, not just the values. Drupal's layer correctly parameterizes values, but the vulnerability suggests that the keys or structural elements of the input can influence the SQL statement construction before parameterization occurs.

For PostgreSQL specifically, this matters because of how the driver handles type casting and query construction. PostgreSQL's type system is stricter than MySQL's, and Drupal's PG driver includes additional logic to handle type coercion — additional logic that creates additional attack surface.

An attacker's crafted request might look something like this at the HTTP level:

http
GET /some-path?filter[field][0]=normal_value&filter[field][1;DROP TABLE users--]=injected HTTP/1.1
Host: target-drupal-site.com

The exact vector depends on which Drupal subsystem passes unsanitized array keys into the database layer. Entity queries, Views exposed filters, and JSON:API filters are all potential entry points since they accept complex structured input from HTTP requests and translate it into database queries.

What makes this particularly dangerous is that it's a second-order injection in the framework itself. The application code looks correct. The developer did everything right. The vulnerability is invisible at the application layer because it lives beneath it.

How to Detect If You're Vulnerable

First, check if you're even running PostgreSQL. Most Drupal sites run MySQL/MariaDB, but if you're on PostgreSQL, you need to act immediately.

Check your database driver configuration in settings.php:

bash
grep -r "pgsql\|postgresql" /path/to/drupal/sites/default/settings.php

If you see 'driver' => 'pgsql' in your database configuration array, you're running the affected driver.

Next, check your Drupal core version. Run this from your Drupal root:

bash
# For Composer-managed sites
composer show drupal/core | grep versions

# Or check the VERSION constant directly
grep -r "const VERSION" core/lib/Drupal.php

Any version prior to the patched release is vulnerable if running PostgreSQL. Check the SA-CORE-2026-004 advisory on drupal.org for the exact patched version numbers for your branch (10.3.x, 10.4.x, 11.x, etc.).

To check for active exploitation attempts, review your web server access logs for unusual array-structured parameters with SQL-like characters in the keys:

bash
# Look for suspicious array key patterns in access logs
grep -iE "\[(.*)(SELECT|UNION|DROP|INSERT|UPDATE|DELETE|--|;)(.*)\]=" /var/log/nginx/access.log
grep -iE "filter\[.*\]\[.*[;'\"\-]" /var/log/nginx/access.log

These patterns aren't definitive proof of exploitation — they could be false positives — but they'll flag requests worth investigating.

Impact Analysis: Who's Exposed and How Bad Is It

The blast radius here is every Drupal site running PostgreSQL that hasn't patched. While MySQL/MariaDB sites make up the majority of Drupal installations, PostgreSQL is common in enterprise deployments, government sites, and organizations that standardize on PostgreSQL across their stack.

Successful exploitation gives an attacker arbitrary SQL execution. On PostgreSQL, that means:

  • Full database read access: user credentials, session tokens, private content, configuration values including API keys stored in config
  • Data modification: elevate any account to admin, modify content, inject malicious data
  • Potential OS-level access: If the PostgreSQL user has elevated privileges or if COPY TO/FROM PROGRAM is available (PostgreSQL 9.3+), an attacker could potentially execute operating system commands

The unauthenticated nature of this vulnerability means automated scanning and exploitation is trivial. Once a working exploit is public — and for Drupal core SQLi, that happens fast — mass scanning will begin within hours. Drupal has been here before (Drupalgeddon in 2014, Drupalgeddon 2 in 2018), and the exploitation timeline was measured in hours, not days.

Warning: If you're running an unpatched PostgreSQL-backed Drupal site and you're reading this days after the advisory, assume compromise. Check for new admin accounts, modified files, and unexpected database changes.

Chain attacks are the real concern. SQLi → admin access → PHP code execution via Drupal's theme/module system → full server compromise. That's a well-worn path for Drupal exploitation.

What to Do About It Right Now

Patch immediately. This is not a "schedule it for next sprint" situation.

For Composer-managed sites, update Drupal core to the patched version:

bash
# Update to the latest patched version
composer update drupal/core drupal/core-* --with-all-dependencies

# Run database updates
drush updatedb

# Clear caches
drush cache:rebuild

# Verify the version
drush status | grep "Drupal version"

Check the SA-CORE-2026-004 advisory for the exact version numbers. Apply the update to your staging environment first if you can, but don't let that delay production patching by more than a few hours.

If you absolutely cannot patch immediately, here are temporary mitigations at the infrastructure level.

For nginx reverse proxy, block requests with suspicious array key patterns:

nginx
# /etc/nginx/conf.d/drupal-sqli-mitigation.conf
# Block requests with SQL characters in array-style parameter keys
if ($query_string ~* "\[.*([;'\"\-]{2}|UNION|SELECT|DROP).*\]=") {
    return 403;
}

# Rate limit complex query parameters
location ~ ^/ {
    # Limit request argument length as a blunt mitigation
    if ($args ~* ".{2048,}") {
        return 413;
    }
}

For Cloudflare WAF, create a custom rule:

yaml
# Cloudflare WAF Custom Rule
# Expression:
(http.request.uri.query contains ";" and http.request.uri.query matches ".*\[.*\].*=.*") or
(http.request.uri.query matches ".*\[.*(UNION|SELECT|INSERT|UPDATE|DELETE|DROP).*\].*=")
# Action: Block

For Vercel deployments (if you're running Drupal behind a Vercel edge), add security headers and consider a middleware that validates query parameter structure:

json
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "X-Content-Type-Options",
          "value": "nosniff"
        }
      ]
    }
  ]
}

Though honestly, for this specific vulnerability, WAF rules are a band-aid. The real fix is the patch. WAF rules can be bypassed with encoding tricks and creative payloads. Patch first, WAF as defense-in-depth.

After patching, verify the fix by checking that the vulnerable code path now properly sanitizes placeholder construction:

bash
# Verify the patch was applied by checking the database driver files
find core/lib/Drupal/Core/Database/Driver/pgsql -name "*.php" -newer /tmp/patch-timestamp

# Or check git log for the security fix
cd web/core && git log --oneline -5

The Bigger Picture: When Your Safety Net Has Holes

This vulnerability follows a pattern I've seen repeatedly: the security mechanism itself becomes the vulnerability. It happened with Drupal before (SA-CORE-2014-005, the original Drupalgeddon, was also a database abstraction layer SQLi). It happens with ORMs, with template engines that promise XSS protection, with authentication libraries that have bypass flaws.

The lesson isn't "don't trust frameworks." You have to trust frameworks — nobody's writing raw SQL with manual escaping for every query in 2026. The lesson is defense in depth isn't optional. Database users should have minimal privileges. Network segmentation should limit what a compromised web server can reach. WAF rules should exist even when you trust your application layer.

For anyone building on Drupal with PostgreSQL: patch today, audit your database user permissions (does your Drupal DB user really need CREATE or DROP privileges?), and set up monitoring for the exploitation patterns I described above. The window between advisory publication and mass exploitation is shrinking with every major CMS vulnerability. Don't be the site that gets popped because patching waited until Monday.

Related posts

Written by Eko

If you found this useful, follow @ekofyi on X for more notes like this — or get in touch if you have a problem to solve.