ZeroPath at Black Hat USA 2026

PostgreSQL CVE-2026-6476: Brief Summary of SQL Injection via pg_createsubscriber Subscription Names

A brief summary of CVE-2026-6476, a high severity SQL injection in PostgreSQL's pg_createsubscriber utility that allows privilege escalation to superuser. Includes patch analysis and affected version details.

CVE Analysis

7 min read

ZeroPath CVE Analysis
ZeroPath CVE Analysis

2026-05-14

PostgreSQL CVE-2026-6476: Brief Summary of SQL Injection via pg_createsubscriber Subscription Names
Experimental AI-Generated Content

This CVE analysis is an experimental publication that is completely AI-generated. The content may contain errors or inaccuracies and is subject to change as more information becomes available. We are continuously refining our process.

If you have feedback, questions, or notice any errors, please reach out to us.

[email protected]

Introduction

A single unescaped identifier in PostgreSQL's pg_createsubscriber utility opened the door for any user with pg_create_subscription rights to escalate to superuser through a crafted subscription name. For organizations running PostgreSQL 17 or 18 with logical replication workflows, this vulnerability (CVSS 7.2) represents a serious privilege escalation path that is straightforward to exploit once the prerequisite access is in place.

Technical Information

The root cause of CVE-2026-6476 is a classic SQL injection flaw: the drop_existing_subscriptions() function in src/bin/pg_basebackup/pg_createsubscriber.c concatenated the raw subscription name directly into SQL command strings without any form of escaping or quoting. The subscription name was interpolated into three distinct SQL statements: ALTER SUBSCRIPTION ... DISABLE, ALTER SUBSCRIPTION ... SET (slot_name = NONE), and DROP SUBSCRIPTION. Because the name was treated as a trusted literal, an attacker who could control the subscription name could inject arbitrary SQL into these commands.

The attack flow works as follows:

  1. An attacker with pg_create_subscription rights creates a subscription with a specially crafted name containing embedded SQL commands.
  2. The malicious subscription name sits dormant in the database catalog.
  3. When pg_createsubscriber is next invoked (for example, during a standby promotion workflow), the utility calls drop_existing_subscriptions() to clean up existing subscriptions.
  4. The function builds its SQL command strings by directly interpolating the subscription name. The injected SQL payload becomes part of the command string.
  5. The commands execute with superuser privileges, giving the attacker full control over the database instance.

The CVSS vector string is AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H. This reflects that while the vulnerability is network exploitable with low attack complexity, it does require high privileges (pg_create_subscription rights) as a prerequisite. The impact across confidentiality, integrity, and availability is rated high across the board, consistent with the superuser execution context.

The vulnerability was reported by Yu Kunpeng. The pg_createsubscriber utility was introduced in PostgreSQL 17, which is why versions before 17 are entirely unaffected.

Patch Information

The official fix was authored by Nathan Bossart, reviewed by Amit Kapila, and committed by Noah Misch on May 11, 2026. It shipped in PostgreSQL 18.4 and 17.10. Two commits carry the fix:

  • c2e44c370 for the PostgreSQL 18 (main) branch
  • d7de7fa84 backported to the PostgreSQL 17 branch

Both commits contain the same logical change. The fix introduces a call to PQescapeIdentifier(), which is libpq's standard function for safely quoting SQL identifiers. Previously, the raw subname variable was interpolated directly into the three SQL statements. After the patch, a new escaped variable subname_esc is produced via PQescapeIdentifier() and used in their place.

Here is the key diff from commit c2e44c370:

@@ -1118,18 +1118,23 @@ drop_existing_subscriptions(PGconn *conn, const char *subname, const char *dbnam { PQExpBuffer query = createPQExpBuffer(); PGresult *res; + char *subname_esc; Assert(conn != NULL); + subname_esc = PQescapeIdentifier(conn, subname, strlen(subname)); + /* * Construct a query string. These commands are allowed to be executed * within a transaction. */ appendPQExpBuffer(query, "ALTER SUBSCRIPTION %s DISABLE;", - subname); + subname_esc); appendPQExpBuffer(query, " ALTER SUBSCRIPTION %s SET (slot_name = NONE);", - subname); - appendPQExpBuffer(query, " DROP SUBSCRIPTION %s;", subname); + subname_esc); + appendPQExpBuffer(query, " DROP SUBSCRIPTION %s;", subname_esc); + + PQfreemem(subname_esc);

Walking through the change:

  1. A new local variable subname_esc is declared to hold the escaped subscription name.
  2. PQescapeIdentifier(conn, subname, strlen(subname)) wraps the identifier in double quotes and escapes any embedded double quote characters, ensuring the result is always a syntactically safe SQL identifier regardless of its contents.
  3. All three appendPQExpBuffer calls now use subname_esc instead of the raw subname, closing the injection vector in the ALTER SUBSCRIPTION ... DISABLE, ALTER SUBSCRIPTION ... SET, and DROP SUBSCRIPTION commands.
  4. PQfreemem(subname_esc) properly frees the memory allocated by PQescapeIdentifier, preventing a memory leak.

This is a clean, minimal, and idiomatic fix. Rather than implementing custom escaping logic, the patch leverages PostgreSQL's own built in PQescapeIdentifier() API, which is the canonical way to safely handle SQL identifiers in client side code. The change is small in scope but critical in impact, completely neutralizing the injection surface in pg_createsubscriber's subscription cleanup path.

Because these are minor version upgrades, a full pg_upgrade is not required. Administrators can stop the Postgres service, install the updated binaries for their specific version, and restart the database.

Affected Systems and Versions

The vulnerability affects PostgreSQL major versions 17 and 18 only:

Major VersionVulnerable VersionsFixed Version
18All versions before 18.418.4
17All versions before 17.1017.10

Versions before PostgreSQL 17 (including 16.x, 15.x, 14.x, and earlier) are not affected because the pg_createsubscriber utility was first introduced in version 17.

The vulnerability specifically requires that an attacker possess pg_create_subscription rights on the target database. Environments where this privilege is restricted to trusted administrators have a reduced attack surface, though patching remains the recommended course of action.

Vendor Security History

PostgreSQL maintains a mature and transparent security response process. The May 2026 security update cycle in which CVE-2026-6476 was addressed also resolved 10 other CVEs and over 60 bugs across all supported major versions (14 through 18). This level of coordinated disclosure and patching reflects the project's commitment to ongoing security maintenance.

PostgreSQL's position as the fourth most popular database technology globally (per DB Engines rankings) and its top ranking among developers who want to use it (47 percent in the Stack Overflow 2025 Developer Survey) means that vulnerabilities in PostgreSQL carry broad ecosystem implications. The project's track record of timely, well documented security releases is a significant factor in maintaining that trust.

References

Detect & fix
what others miss

Works with
  • GitHub
  • GitLab
  • Bitbucket
  • Azure DevOps Services
  • Jira
  • Linear
  • Slack
  • Security Compass
Security magnifying glass visualization