PostgreSQL Logical Decoding Flaw Let a Replication Account Take Over the Server for 12 Years

Ethical Hacking Complete Course Zero to Expert
Hack like black hat hackers. Penetration testing, Kali Linux, WiFi and web hacking, and the hacker mindset behind it.
→ Take the full courseA database account that was only allowed to copy data could run commands on the server itself. The admins never gave it that right. The hole has been open since PostgreSQL 9.4 shipped in 2014, twelve years.
That account has one job. It has no rights on your tables and it cannot create or drop anything. It gets permission to follow along with what changes in the database, because a backup tool or a standby server has to know what happened. In PostgreSQL that permission is called REPLICATION.
The name makes it sound harmless. It is not. The same attribute lets you run pg_basebackup, and a base backup copies the data directory in one go, pg_authid and the password hashes with it. PostgreSQL scored this flaw at 7.2, with Privileges Required set to High.
It was still enough to load a file from another machine into the database process and run commands as the system user.
A database writes down what it is about to change before it changes it. Inserts, updates and deletes land in a log first, so the work survives a power cut. That log is the write-ahead log.
Logical decoding reads that log and turns it into a stream other systems can follow. A small piece of code decides what the stream looks like: pgoutput for the native format, test_decoding for plain text, wal2json for JSON. That piece is the output plugin, and the client picks one by name when it opens a slot:
| |
The loader took that name as given.
Loading a plugin means loading code straight into the running database. PostgreSQL has a command for exactly that, LOAD, and it is watched. Ask for a file you have no business asking for, and for a user without superuser rights a check called check_restricted_library_name() stops you.
The replication path skipped that check. The plugin name came in over the wire, went to LoadOutputPlugin(), and from there to the call that opens a library on disk: dlopen() on Linux and macOS, LoadLibrary() on Windows. Slashes, backslashes and ../ came through untouched. One loader, two ways in, and the check sat on only one of them.
On Windows a filename can point at another computer. Two backslashes, a machine name, a share: \\attacker\share\evil.dll. Windows sees that and goes to fetch it over SMB on port 445. It pulls the DLL across, maps it into the running database and calls _PG_init() to start it.
Nothing lands on the target’s disk. A scanner looks for a file that was never written.
Linux and macOS have their own version of that trick. Some machines run a service that mounts a network share the moment anything touches the path. It is called autofs. A plugin name like ../../../net/attacker-ip/share/evil.so is enough: the mount happens over NFS on port 2049 and the library loads from there. Not standard on a hardened server, but common on machines someone set up years ago and never looked at since.
Once the code is in, it sits inside the database process, in the same memory as the server, with nothing between it and the rest of the machine.
PostgreSQL keeps its users in a table called pg_authid, with a column for each right they hold. Normally you change that with ALTER ROLE and the server checks whether you are allowed to. The plugin writes into the table directly and skips the check. It turns the rights on for its own account and hangs a hook in the permission code that answers yes to whatever it gets asked.
Look at the roles afterwards and it reads like an ordinary ALTER ROLE. The log stays quiet.
Then it digs in:
- โ it rewrites
pg_hba.conf, the file that decides who may log in, so a login goes through with whatever user and password gets offered, and reloads it on the spot - โ it copies itself somewhere permanent and adds itself to
shared_preload_libraries, the list of code the server loads at startup, so it comes back after a restart - โ it puts the rights back if an administrator takes them away
Vladimir Tokarev and Yu Kunpeng reported the problem to the PostgreSQL project on February 21, 2026, with a root cause analysis and a working Windows proof of concept. The fix and the advisory went out together on August 13, in a release that carried 28 CVEs and more than 110 bug fixes, so this one sat somewhere in the middle of a long list. The technical write-up followed on September 1, from Cyera, under the name PostGREShell.
The fix is a list. A new setting, output_plugin_libraries, names the plugins a replication client may ask for. Anything else gets refused, superuser or not. The default:
| |
If you run wal2json or decoderbufs, your logical replication stops after the update until you put that plugin in the list yourself. The wal2json project now says so in its own README, with the CVE number next to it. Debezium ships with decoderbufs as its default, so that setup stops as well, while Debezium on pgoutput keeps running.
Coming from 17 or newer, pg_upgrade --check fails too if the new cluster does not allow the plugins your old slots use. That is the kind of surprise that gets an update postponed. Postponing is how a twelve-year-old hole stays open.
The fix has a gap of its own. On September 3 a Fujitsu developer reported on the PostgreSQL hackers list that pg_createsubscriber builds its slots with pgoutput without checking the new list first, so a dry run passes and the conversion itself stops dead:
| |
A day later came the second half of the problem. The setting can differ from one database to the next, and the first patch only checked the database from the connection details, not the ones the tool was told to convert. The patch was still in review on September 6.
The same research included a hunt on VirusTotal: 114 malicious PostgreSQL plugins already sitting in the collection. That number is not evidence of an attack with this flaw. No weaponized exploit sits in a public repository, and the CVE is missing from the CISA list of vulnerabilities under active attack. The trigger is no secret either: the write-up says it fits in three lines of Python. What those 114 files show is that someone is already building hostile database plugins and uploading them.
Two things had to be true. Someone needed an account carrying REPLICATION, and the server had to run with wal_level = logical. That is not the default, replica is, and switching it takes a server restart, so somebody turned it on for a reason: change data capture, a migration, analytics that follow the database live.
What to check on your own machine:
- โ before you update, run
SELECT DISTINCT plugin FROM pg_replication_slots WHERE plugin IS NOT NULL;and make sure those plugins end up in the list - โ open
psqland run\du, then read which roles carry Replication - โ run
SHOW wal_level;and see whether it sayslogical - โ update to 18.6, 17.11, 16.15, 15.19 or 14.24, then run
SHOW output_plugin_libraries; - โ check that setting per database, because another database on the same server can carry a different list
- โ take
REPLICATIONaway from accounts that do not need it - โ limit the replication lines in
pg_hba.confto addresses you know - โ block outbound traffic on 445 and 2049 from the database server
- โ if you are on PostgreSQL 14, plan the move: that branch stops getting fixes on November 12, 2026
A database handed out a limited account. That limited account could name a file on someone else’s SMB share and have the server load it into its own process. For twelve years the code did exactly what its instructions said. The permission says replication. It handed out a shell.
If you run PostgreSQL, open psql tonight, type \du and look at which roles carry Replication that you never remember handing out.
My Ethical Hacking Complete Course Zero to Expert takes you there step by step: reconnaissance, scanning, exploitation and traffic analysis, hands-on, from your first day with no Linux or hacking background.
โ Join my complete ethical hacking course
Hacking is not a hobby but a way of life.
Sources:
PostgreSQL Security Advisory | PostgreSQL 18.6 Release Notes | PostgreSQL Documentation | Cyera Research | pgsql-hackers
Stay updated
Get the latest posts in your inbox every week. Ethical hacking, security news, tutorials, and everything that catches my attention. If that sounds useful, drop your email below.