Back to Blog

REPLICATION was never a read-only privilege: PostgreSQL closed a twelve-year dlopen()

Two identical rack servers pulled out on their rails in a small server room

The account somebody requested in a ticket with the sentence "it only needs to read the WAL" could load any file on the system and run its code as the user running the database. That is not our reading: it is the official description of CVE-2026-6471, published by the PostgreSQL project itself on 13 August. It was like that for twelve years.

We design and run infrastructure and application platforms for clients, and a database with its replica and some change consumer hanging off it is a thoroughly ordinary piece of those setups. Which is why this CVE interests us less for the hole — that closes with an update — and more for two things that outlive the patch: what people thought they were granting when they granted that privilege, and what breaks the day you apply the update without reading the release note.

What an account with REPLICATION could actually do

The project's own description needs no interpretive translation: "Missing authorization in PostgreSQL logical decoding allows a non-superuser holding REPLICATION privilege to dlopen any file visible to the operating system account running the server, via the choice of logical decoding plugin. This in turn runs arbitrary code as that account."

The mechanism fits in two sentences. When a client creates a logical decoding slot, it states which plugin it wants its changes served through; that name is a string the client chooses. Until the patch, that string reached the system's dynamic loader without anyone checking whether the file it pointed at had anything to do with PostgreSQL. You need the server to be running with wal_level = logical — physical replication loads no plugins — and you need the REPLICATION attribute on the role. Nothing else.

Cyera's research, which is where the long write-up of the flaw lives under the name PostGREShell, adds the uncomfortable detail: the file does not have to be on the machine already. On Windows they describe UNC paths — reaching the attacker's port 445 is enough — and elsewhere they bound the ground carefully: RHEL-family systems with autofs and macOS before Catalina, where NFS automounting comes as standard. And once the code runs as the system user, they describe the path to database superuser by writing to catalogs directly. That stretch is theirs and we cite it as such: we have not reproduced it.

The manual had been warning for twelve years; the ticket said something else

The CREATE ROLE documentation does not hedge: "A role having the REPLICATION attribute is a very highly privileged role, and should only be used on roles actually used for replication." It is right there, on the page anybody creating the account reads. And still, the attribute gets granted daily in serious organisations without anyone blinking.

The reason is the usual one, and it has nothing to do with laziness: the privilege is requested for what it does and granted for what it is called. The person asking writes "I need to set up change data capture into the warehouse" or "the backup tool needs to read the WAL". The person approving reads "replication" and understands "reads, does not write". Both are right about their own sentence and both are wrong about what just happened, because the attribute does not describe an operation, it describes a channel: connect in replication mode and create slots. What the slots then did was, until August, a different conversation that nobody had.

It is the same pattern we described in the Gitea case where write access was handed over by the signup form: nobody decided to grant that, it granted itself because the name of the checkbox sounded harmless.

The 7.2 is not the number that decides

The CVE scores 7.2, and the vector explains why it does not go higher: PR:H, high privileges required. The metric is applying the manual to the letter — REPLICATION is a highly privileged role — so on paper the attacker already had to be someone trusted. In practice, that attribute lives in service accounts: backup tools, standby servers, change data capture pipelines and monitoring systems that read the WAL, which is the list Cyera enumerates. The score describes the door accurately; what it cannot know is how many keys you handed out.

And now the part we are not going to overstate, because we have a habit of looking before rushing anyone. As of 4 September, The Hacker News checked two things and published both: this CVE was not in CISA's known exploited vulnerabilities catalog, and there was no public proof-of-concept code in repositories. Cyera, for its part, says it found 114 malicious PostgreSQL plugins in the wild — trojans, miners and reverse shells — in a VirusTotal threat hunt, and is honest about what that means: it links none of them to exploitation of this CVE. Worth underlining, because the headline invites the leap: malicious libraries for PostgreSQL existing proves that people are writing them and that the loading mechanism was already a known target. That is a different claim, and it is the one on the table. The same applied to last week's two Artifactory CVEs, where the one that entered the exploited catalog first was the medium-scored one, not the critical.

The patch moves the boundary

The fix does not take capabilities away from the role: it adds a server parameter. It is called output_plugin_libraries and the documentation defines it as the list of libraries installed in dynamic_library_path "that are also trusted for use as logical output plugins by replication clients". Any request naming anything else is refused, and the documentation adds a sentence worth reading slowly because it takes in superusers too: "All users are subject to this restriction."

It is worth spelling out what that means in governance terms, because it is the part no headline carries. Before, who could get code loaded into the server was decided by a role attribute, and roles get created and modified by quite a few people. Now it is decided by a list only superusers touch: in the source, the parameter is declared PGC_SUSET and carries the GUC_SUPERUSER_ONLY flag, so an ordinary account cannot even read it. In practice it lives in postgresql.conf, but there is still a path through SQL: a superuser can run ALTER SYSTEM SET output_plugin_libraries and reload without going down to the machine. There are far fewer hands there, and that is the real improvement. What does not change is that the control has moved somewhere that, if your database configuration is neither versioned nor reviewed, nobody looks at either.

And this is where the security patch stops your replication

The default value of the list is two names: pgoutput and test_decoding, the two plugins shipped with PostgreSQL. The 18.6 release notes say so in full, warning included: "Installations that rely on other output plugins must add them after updating the server". Translated into consequences: if your change data capture runs on wal2json or decoderbufs, once you restart on the new version logical decoding refuses to work until somebody edits the configuration.

The wal2json project already documents this in its own README, with the line you have to write — output_plugin_libraries = 'pgoutput, test_decoding, wal2json' — and with the error you will see if you do not: library "wal2json" may not be used as an output plugin. The operational good news is that the parameter takes a configuration reload: pg_ctl reload or SELECT pg_reload_conf(), without stopping the server again. And the message is one of the good ones: the documentation shows it landing in the server log with an ERROR, a DETAIL carrying the list's current value, and a HINT that literally tells you to add the library and reload. The problem is not that the system fails to say it; it is that it says it in the log of a server nobody is watching at that hour.

On decoderbufs it is worth being precise, because it is the one that can take the most deployments down with it: the documentation for Debezium's PostgreSQL connector lists decoderbufs and pgoutput as options for plugin.name and gives decoderbufs as the default value. Whoever switched it to pgoutput by hand notices nothing. Whoever left it as it came, does. There is a second place it fires, and this one bites during migrations: pg_upgrade --check fails if the new cluster does not permit the plugins of the old cluster's slots, when migrating from 17 or later. It is a well-meant failure — it warns you beforehand — but it arrives at the least amusing moment possible, with the window open and the clock running.

All of this travels inside a minor security update, the kind many organisations apply automatically through their distribution channel. It is the same way of breaking production we wrote about in August with the .NET patch that took WPF application printing down, with one difference in PostgreSQL's favour that is only fair to acknowledge: here the project documents it, explains it and leaves the way out written on one line. That the series also went from 18.4 to 18.6 because 18.5 was pulled over a regression — the announcement says so itself — is the unintentional summary of the whole thing: minor versions are not paperwork, not even for the people shipping them.

From that same 13 August batch — 28 CVEs at once — we already wrote about another one that behaved just as quietly: pgcrypto storing cleartext in the column you thought was encrypted. Two flaws from the same release, sharing one property: the INSERT did not fail then and the server does not shout now.

Four queries that answer whether this touches you

They take five minutes per instance and answer the article's two questions: who you gave it to, and what is about to stop.

  • SELECT rolname FROM pg_roles WHERE rolreplication; — who holds the attribute today. Add the superusers to that list in your head; they carry it implicitly.
  • SHOW wal_level; — if it does not say logical, logical decoding is not in play on that instance and this particular flaw does not reach you.
  • SELECT slot_name, plugin, active FROM pg_replication_slots; — the plugin column tells you which names you will have to put in the whitelist. Running this before you update is the difference between a boring window and a phone call; the documentation itself in fact includes a version of this query for building the list from your persistent slots.
  • SHOW output_plugin_libraries; — after updating, to see what the server actually permits. Mind who runs it: the parameter is flagged superuser-only, so from the application account you get an error rather than the value; you need a superuser or membership of pg_read_all_settings. And outside SQL: the replication lines in your pg_hba.conf, which is where Cyera recommends limiting those connections to trusted addresses.

One question remains that no query answers, and it is the one that really defines your surface after the patch: who can write to postgresql.conf and to the machine's library directory. If the answer is "I don't know" or "anyone with server access", the whitelist is a door with the key left in on the inside.

The order we would do it in

First the inventory of slots and plugins on every instance, including the pre-production ones — the ones nobody looks at and the first to update themselves. Then, in the same change that raises the version, the output_plugin_libraries line with what you already use; not in a second change "once we see it fail". Then, take the attribute away from whatever does not replicate: ALTER ROLE … NOREPLICATION, reversible in one line, applied to the accounts that came out of the first query and that nobody can explain. And finally the alert, which is the part that is almost never there.

We insist on the alert because the cost of not noticing is not only that data fails to reach its destination. A slot that exists and does not advance retains WAL on the server's disk, and there the clock runs by itself: first it is a graph going up, then it is a full partition, and a full partition on a database is not a replication incident, it is an outage. There is a brake for that, max_slot_wal_keep_size, and it is worth knowing that it ships set to -1: no limit. Giving it a value changes how you fail — the slot becomes unusable instead of filling your disk — and that is a decision taken beforehand, not at three in the morning. The useful alert is not "the service is up", it is slot lag growing. We monitor with Zabbix and the rule we follow has not changed in years: alerts that matter, not noise — and this is one that matters, because it warns of an outage with hours to spare. If you have somebody on call 24×7, that metric has to be on their screen; if you do not, it has to be in the inbox of somebody who looks on a Sunday. And in 18 there is a third parameter worth knowing before you need it, idle_replication_slot_timeout, which ships disabled.

And a rule of conduct that costs nothing: the day a vendor adds a new parameter with a restrictive default, that version does not get approved without reading the whole note. A new parameter with a restrictive default is, by definition, a behaviour change dressed as a security fix. It applies to PostgreSQL and it applies to everything else running on the data and applications platform your business depends on.

What is left once the patch is in

The hole closes by updating and there is no more to it. What does not close by updating is the habit that made it interesting: granting attributes for the name they carry on the form rather than for what they permit. Twelve years on, the correct answer to "who has REPLICATION?" is still, in too many places, "the usual people, I suppose". Zero Trust is not buying a product: it starts with being able to answer that question with a list, and continues with the list being short.

Do you know who holds REPLICATION on your databases?

We review service account privileges, update windows and replication alerting as part of the job, with a Zero Trust approach: no implicit trust because of what a role is called. We do not resell anyone's products, so a recommendation to remove a privilege — or to leave it where it is — does not benefit us either way.

Talk to everyWAN

Note on sources

The description of CVE-2026-6471, the 7.2 score with vector AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H and the affected and fixed versions (18.6, 17.11, 16.15, 15.19 and 14.24, released on 13 August 2026) come from the project's official page at postgresql.org/support/security. The credit for the finding is the one published in the 18.6 release notes: "The PostgreSQL Project thanks Vladimir Tokarev and Yu Kunpeng for reporting this problem". The quote about the REPLICATION attribute is from the CREATE ROLE documentation; the definition of output_plugin_libraries, its default value, the ERROR/DETAIL/HINT log block and the query for building the list come from the PostgreSQL 18 replication configuration documentation, as do max_slot_wal_keep_size and idle_replication_slot_timeout. That the parameter is PGC_SUSET with the GUC_SUPERUSER_ONLY flag we read in src/backend/utils/misc/guc_tables.c on the 18 branch of the PostgreSQL repository, not in the documentation. The withdrawal of 18.5 over a regression is in the official announcement of 13 August. The text about the whitelist, the configuration example and the pg_upgrade --check warning are from the 18.6 release notes. The error library "wal2json" may not be used as an output plugin and the example line with wal2json are from the wal2json project's own README. The plugin.name values for Debezium's PostgreSQL connector are from its documentation. The name PostGREShell, the detail about UNC paths and NFS automounting, the figure of 114 malicious plugins on VirusTotal, the list of tools that typically carry the attribute and the pg_hba.conf recommendation come from the research published by Cyera, which does not link those plugins to this CVE; underlining that distinction is ours. The check that the CVE was not in CISA's KEV catalog and that no public proof of concept existed as of 4 September 2026 is from The Hacker News. The twelve years are counted from the introduction of logical decoding in PostgreSQL 9.4, released in December 2014, so we are rounding up by a few months. The SQL queries are standard and anyone can run them against their own instance to check what we say.

PostgreSQL Zero Trust Data Patching
Share LinkedIn X

Subscribe to our newsletter

To receive IT stories, everyWAN news and exclusive subscriber offers, sign up to our mailing list

Minorisa de Sistemas Informaticos y Gestión S.L. © 2026
everyWAN
everyWAN