[{"data":1,"prerenderedAt":4},["ShallowReactive",2],{"post-content-postgresql-cve-2026-6471-postgreshell-replication-audit":3},"\u003Cp>Monday night I did something I'd been putting off: I actually read the Cyera write-up on PostGREShell instead of skimming the headlines. Twenty minutes later I was SSHing into our production database box at 11pm, running \u003Ccode>SELECT rolname FROM pg_roles WHERE rolreplication;\u003C\u002Fcode> and holding my breath a little. If you run PostgreSQL, you should probably do the same tonight.\u003C\u002Fp>\n\n\u003Cp>The short version: \u003Cstrong>CVE-2026-6471\u003C\u002Fstrong> lets an account with the REPLICATION privilege run arbitrary code on the database server. Not superuser accounts. Regular, boring replication accounts, the kind you hand out to backup tools, CDC pipelines, and monitoring systems. Cyera found the flaw had been sitting in PostgreSQL's logical decoding since 9.4 shipped in 2014. Twelve years. It's fixed now in 18.6, 17.11, 16.15, 15.19, and 14.24, but there's an upgrade gotcha that will quietly break your CDC pipeline if you don't know about it.\u003C\u002Fp>\n\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1526374965328-7f61d4dc18c5?w=1200&h=630&fit=crop&q=80&auto=format\" alt=\"Dark server room with racks of database hardware\" loading=\"lazy\">\u003C\u002Fp>\n\n\u003Ch2>How the bug works\u003C\u002Fh2>\n\n\u003Cp>Logical decoding is how external tools read changes out of the write-ahead log. A tool creates a replication slot and names an output plugin that formats the changes. Here's the part that made me wince: the plugin name from \u003Ccode>CREATE_REPLICATION_SLOT\u003C\u002Fcode> gets passed straight to the OS library loader. On Linux and macOS that's \u003Ccode>dlopen()\u003C\u002Fcode>, on Windows it's \u003Ccode>LoadLibrary()\u003C\u002Fcode>. No validation. No sanitization.\u003C\u002Fp>\n\n\u003Cp>PostgreSQL does have a check for this. The SQL \u003Ccode>LOAD\u003C\u002Fcode> command passes a \u003Ccode>!superuser()\u003C\u002Fcode> flag that triggers path validation before anything gets loaded. The replication path passes nothing. The protocol parser even accepts almost any character inside a quoted plugin name, so slashes, \u003Ccode>..\u002F\u003C\u002Fcode> traversal, and Windows UNC paths all survive the trip. Cyera's summary was blunt: the whole bug is code execution via \u003Ccode>dlopen()\u003C\u002Fcode>.\u003C\u002Fp>\n\n\u003Cp>Once code runs inside the backend process, it runs as the postgres OS user. In their demo, the researchers wrote the role catalog directly and turned their replication account into a permanent superuser, plus three persistence tricks that survived a restart.\u003C\u002Fp>\n\n\u003Ch2>Why replication accounts are the soft spot\u003C\u002Fh2>\n\n\u003Cp>Every serious Postgres deployment has at least one. pgBackRest or WAL-G for backups, a Debezium pipeline for change data capture, maybe a standby in another region. All of them hold the REPLICATION attribute, and most people file those credentials under \"operational plumbing\" mentally. Low risk, limited blast radius.\u003C\u002Fp>\n\n\u003Cp>That assumption is what makes this nasty. Exploitation needs three things: an account with REPLICATION, \u003Ccode>wal_level = logical\u003C\u002Fcode>, and a way to get a malicious library in front of the server. Windows is the most direct path since a UNC path over SMB (port 445) fetches the DLL from a machine the attacker controls without touching the disk. Linux and macOS need NFS automounting enabled. Otherwise the attacker needs to plant a file on the server first, which is still a plausible move once they have any foothold in your network.\u003C\u002Fp>\n\n\u003Cp>Two details worth knowing. Cyera searched VirusTotal and found 114 malicious PostgreSQL plugins already in the wild, including miners and reverse shells. And there's no public PoC for this specific CVE yet, which means the window for patching calmly is open but shrinking.\u003C\u002Fp>\n\n\u003Ch2>The audit I ran\u003C\u002Fh2>\n\n\u003Cp>Three queries, in this order. I keep them in Snippet Ark now since I expect to run them quarterly from here on.\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-sql\">-- Who has REPLICATION? Trim anything that doesn't need it.\nSELECT rolname, rolcanlogin FROM pg_roles WHERE rolreplication;\n\n-- Is logical decoding even on? If it says 'replica', remote\n-- exploitation is off the table for this bug.\nSHOW wal_level;\n\n-- What plugins are actually in use?\nSELECT slot_name, plugin, active FROM pg_replication_slots\nWHERE plugin IS NOT NULL;\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>The first query is where you'll likely find surprises. We had a replication account created in 2021 for a migration project that ended two weeks later. The account never left. If yours has that too, this is a good excuse.\u003C\u002Fp>\n\n\u003Ch2>The upgrade gotcha\u003C\u002Fh2>\n\n\u003Cp>Now the part that will bite people mid-upgrade. The fix introduces a whitelist parameter called \u003Ccode>output_plugin_libraries\u003C\u002Fcode>, and it defaults to just \u003Ccode>pgoutput\u003C\u002Fcode> and \u003Ccode>test_decoding\u003C\u002Fcode>. If you run anything else, wal2json and decoderbufs being the common ones, logical decoding gets refused after you upgrade until an administrator adds the library and reloads.\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-ini\"># postgresql.conf\noutput_plugin_libraries = 'pgoutput, test_decoding, wal2json'\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>A reload is enough, no restart needed. If you forget, the failure is visible in the server log:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-text\">ERROR:  library \"wal2json\" may not be used as an output plugin\nHINT:  Add it to output_plugin_libraries\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Check your slots \u003Cem>before\u003C\u002Fem> upgrading, not after your CDC pipeline pages you. One more version note: 18.5 was skipped entirely because a regression was caught before release, so don't be confused when 18.4 jumps straight to 18.6.\u003C\u002Fp>\n\n\u003Ch2>Locking the door anyway\u003C\u002Fh2>\n\n\u003Cp>Even patched, this changed how I look at database servers. Three cheap hardening moves: restrict replication connections in \u003Ccode>pg_hba.conf\u003C\u002Fcode> to the specific hosts that need them, and block outbound SMB (445) and NFS (2049) from database boxes. Neither should ever leave a database server. On Linux, turn off autofs if nothing uses it.\u003C\u002Fp>\n\n\u003Cp>The uncomfortable lesson here isn't really about PostgreSQL. It's that we treat \"backup account\" as a synonym for \"harmless account,\" and attackers know it. Your service credentials deserve the same audit energy as your admin accounts. I fixed our zombie migration account at 11:40pm and went to bed feeling weirdly good about it.\u003C\u002Fp>\n\n\u003Cp>If you're into database performance as well as database security, I wrote earlier about \u003Ca href=\"\u002Fposts\u002Fredis-caching-patterns-web-developers\u002F\">Redis caching patterns\u003C\u002Fa>, which pairs nicely with this one for anyone running a full data stack.\u003C\u002Fp>\n",1789138766806]