8 min readJohnny UnarJohnny Unar

Your Replication Account Is the Blast Radius

CVE-2026-6471 turns any REPLICATION-attributed Postgres account into RCE. Here's how to audit the credentials nobody has looked at since 2019.

the credential you forgot about

Cyera dropped CVE-2026-6471 on September 1st, and the part that should ruin your Saturday isn't the RCE itself, it's where the RCE lives. Logical decoding in PostgreSQL has been missing an authorization check since version 9.4 shipped in 2014, which means every version from then through 18 lets any account carrying the REPLICATION attribute point logical decoding at arbitrary OS-visible files, load a plugin, and walk straight to code execution as the postgres OS user. That's superuser escalation, arbitrary file read, and a persistent backdoor, all from an account that most teams treat as a second-class citizen.

Think about who actually holds REPLICATION in your cluster. It's almost never your app user, because you were careful there, you scoped that role down to specific tables and revoked everything you could. It's not your DBA login either, because that one has MFA on the bastion and gets rotated. The REPLICATION credential is the one you created in a hurry three years ago so Debezium could stream CDC into Kafka, or the one your pg_basebackup cron job uses at 2am, or the one your monitoring agent needs to read replication lag. You created it, you got the pipeline green, and you never looked at it again.

And that credential is often the least protected thing in the whole system. It's frequently in a plaintext connector config, checked into a Helm values file, or sitting in an environment variable on a box that fifteen people can SSH into. Nobody audits it because it doesn't touch customer data directly, so it falls through the gap between the security team who audits app access and the platform team who assumes the DBAs own it. That gap is now the blast radius.

what logical decoding actually hands an attacker

To understand why this is so bad you have to understand what wal_level = logical really turns on. Physical replication just streams WAL bytes to a standby, and while REPLICATION accounts can already do plenty of damage in that mode, logical decoding is a different beast because it runs an output plugin inside the backend process that reads the WAL and transforms it into a logical change stream. That output plugin is a shared library. It gets loaded by name. And until this patch, the code path that loads it never checked whether the caller had any business loading arbitrary libraries.

So the attack, roughly, is that an account with REPLICATION connects, creates a logical replication slot, and specifies an output_plugin that resolves to a file the attacker controls or to a system library that can be abused. Combine that with the ability to have the server read OS-visible files during the process and you get file disclosure of anything the postgres user can read, which on a typical box includes SSH keys, other service credentials in /etc, and the contents of the data directory itself. From there escalation to full RCE is mechanical.

The cruel detail is that lots of teams turned on wal_level = logical years ago for one Debezium connector and forgot it was a global setting. It's not per-table, it's not per-database, it's cluster-wide. Once it's on, every REPLICATION account on that instance can reach the logical decoding machinery, whether or not it was ever meant to do CDC. You enabled it for one pipeline and quietly widened the attack surface for the entire cluster.

enumerate every replication account first

Before you touch anything, find out how bad your exposure actually is, because most teams genuinely do not know how many REPLICATION roles they have. The attribute doesn't show up in the places people usually look. Run this:

SELECT rolname, rolsuper, rolreplication, rolbypassrls FROM pg_roles WHERE rolreplication OR rolsuper;

Superusers implicitly have replication, so both columns matter. Now correlate that against pg_hba.conf, because a role having REPLICATION is only exploitable if something can actually authenticate with it over a replication connection or a normal one. Grep your hba file for the literal keyword:

grep -nE 'replication' $(psql -tA -c 'SHOW hba_file')

And check pg_stat_replication and pg_replication_slots to see which of these accounts are genuinely in use right now versus abandoned:

SELECT slot_name, plugin, slot_type, active, active_pid FROM pg_replication_slots;

We did this exercise on a client's cluster last month and found four REPLICATION roles. One was Debezium, one was a standby that had been decommissioned in 2023 but whose credential was still valid, one was a monitoring agent that only needed pg_stat access and never should have had REPLICATION at all, and one nobody could identify. That last one is the whole point. If you can't name the human or system that owns a REPLICATION credential and explain why it needs that attribute, you drop it now with ALTER ROLE foo NOREPLICATION and see what breaks. Almost nothing will, because monitoring agents reading replication lag through pg_stat views don't need the attribute, only actual replication consumers do.

what the patch actually changed

The fix isn't just an authorization check bolted onto the old code path, it introduces a new GUC called output_plugin_libraries that acts as an allowlist for which shared libraries logical decoding is permitted to load. By default, after patching, the allowed set is empty except for the plugins that ship with Postgres itself, which means an attacker specifying an arbitrary library name gets rejected before anything loads. If you run pgoutput for native logical replication or a vendored plugin like wal2json, you have to explicitly list them, and that's the correct tradeoff because it forces you to declare exactly which decoding libraries your cluster is allowed to touch.

So after you patch to the fixed minor release for your major version, don't assume your CDC pipeline keeps working. If you use wal2json or decoderbufs or anything that isn't pgoutput, you'll need something like:

output_plugin_libraries = 'pgoutput,wal2json'

in postgresql.conf, and a reload. Test this in staging before you patch prod, because the failure mode is your Debezium connector suddenly failing to create slots with an error about the plugin not being permitted, and if you find that out during a 2am incident you will be unhappy.

The allowlist is genuinely good design because it converts an implicit trust boundary into an explicit one. Before, the server would load anything you named. Now it loads only what you declared, which is how this should have worked in 2014.

harden pg_hba.conf so unpatched isn't fatal

Patching is the real fix, but if you're running a managed Postgres where the vendor hasn't rolled the fixed minor yet, or you have a fleet where patching takes a week, you can meaningfully reduce exposure at the authentication layer, because the attack requires a working replication or superuser connection and pg_hba.conf decides who gets one.

Start by making replication lines as narrow as physically possible. A depressingly common pattern looks like this:

host replication all 0.0.0.0/0 md5

which lets any REPLICATION role authenticate from anywhere. Kill that. Replication should be locked to the exact CIDR of your standbys or your CDC host, and it should use scram-sha-256, never md5, and ideally client certificate verification:

hostssl replication debezium 10.20.4.11/32 cert clientcert=verify-full

The blast radius shrinks a lot when the credential is useless unless it's coming from one specific host presenting one specific client cert. Even if the connector config leaks, the attacker also needs network position and a valid cert to do anything with it.

One subtlety people miss: logical decoding can be driven over a normal database connection, not only over a dedicated replication connection, so tightening only your 'replication' hba lines isn't sufficient if the same role can connect as a regular client and issue pg_create_logical_replication_slot. Scope the role's ordinary connectivity too, and if a CDC account only ever connects from one host, pin its normal database lines to that same /32. Combine narrow hba rules with NOREPLICATION on every role that doesn't strictly need it and you've turned a trivial exploit into one that requires an attacker to already be sitting on your standby subnet with a stolen cert, which is a very different conversation with your incident team.

the actual lesson

This CVE is a good excuse to fix a category of problem, not just a single bug. Machine credentials that get created once during a project and then live forever are where a lot of real breaches actually start, and they share a nasty property: they're too boring for the security team to audit and too infrastructural for the app team to own, so they rot in the gap between.

When we do security work for clients at steezr, the replication and service accounts are almost always the part of the review that surprises people, because everyone remembers to scope down the app role and nobody remembers the connector credential from the migration they did two years ago. Build the audit into something recurring. A quarterly job that dumps every rolreplication and rolsuper role, cross-references it against active slots, and pings a human to justify each one takes an afternoon to write and catches the abandoned standby credential before it becomes CVE-2026-6471's entry point.

Patch to the fixed minor, set output_plugin_libraries, drop REPLICATION from everything that doesn't consume replication, and pin your hba rules to specific hosts with scram and certs. Then write down who owns each of the credentials that survive, because the next logical-decoding bug is coming, and the accounts nobody remembers are the ones that will get you.

Johnny Unar

Written by

Johnny Unar

Want to work with us?

CVE-2026-6471 turns any REPLICATION-attributed Postgres account into RCE. Here's how to audit the credentials nobody has looked at since 2019.