PgBouncer 1.26.0 came out on September 23 and fixes three CVEs. Two of them can be triggered by anyone who can open a TCP connection to the listener. They need no password and no valid username. If PgBouncer sits anywhere a hostile packet can reach it, upgrade before you finish reading this.
Now that you’ve started the upgrade, here is what you are upgrading away from.
The one-line crash
SCRAM-SHA-256 is a four-message handshake. The client sends a nonce; the server sends back the combined nonce, a salt, and an iteration count; the client sends the combined nonce again plus a proof; the server verifies the proof and sends its own. That second client message, the client-final-message, carries three attributes: c= for channel binding, r= for the nonce, and p= for the proof.
PgBouncer’s parser did not require the r=. Omit it and the parser reports success with the nonce pointer still NULL, and the next thing that touches it dereferences it. That is CVE-2026-19888, and it has been in every release since SCRAM support arrived in 1.11.0, in August 2019.
Two details make this worse than the usual parser bug. The crash happens before any credential is checked, so a valid account is not required. And PgBouncer, like PostgreSQL itself, runs a mock SCRAM exchange for usernames that don’t exist, so that attackers can’t enumerate roles by timing. That defense is what makes the crash reachable with any username at all. (It was reported independently by six different people, which tells you something about how much automated attention protocol parsers are getting this year.)
“I use auth_type = md5, so this doesn’t apply to me.” It almost certainly does. md5 is PgBouncer’s default auth_type, but password_encryption has defaulted to scram-sha-256 since PostgreSQL 14, so the secrets in pg_authid are SCRAM secrets, and PgBouncer upgrades the login to SCRAM when it finds one. The CVE record lists exactly that as an affected configuration. The only workaround it offers is “restrict network access to the listener,” because there is no setting that avoids the bug.
The one that doesn’t crash
CVE-2026-6668 is an integer overflow in the packet buffer growth logic. Send enough data into a single packet buffer, before authenticating, and the size computation wraps, the growth loop never finds its exit condition, and PgBouncer pins a core forever. It is reachable because the default max_packet_size is 2147483647, which is to say “no limit anyone will ever hit by accident.”
This one is arguably worse than the crash. A crash, at least, gets noticed by systemd. A hang does not. PgBouncer is single-threaded, so every client and every pool it fronts simply stops, and stays stopped until a human kills the process. If your health check does not run a query through the pooler, you find out when the application pages you.
The third, CVE-2026-6669, requires a malicious PostgreSQL server advertising an absurd SCRAM iteration count, which PgBouncer would dutifully grind through while every other client waited. It now caps the count at 1,000,000. If you only point PgBouncer at servers you control, this is the least urgent of the three. If you don’t, that is a separate conversation.
Why this is worse than it sounds
PgBouncer is one process. Every client connection and every server connection live in one address space, serviced by one event loop. Crash it and every application connection drops at the same instant. Then every application reconnects at the same instant, and if your supervisor restarts PgBouncer promptly, it comes back with empty pools and opens a fresh server connection for each of them, all at once, against a PostgreSQL that just lost every session it had. The thing PgBouncer exists to prevent, a connection storm, is precisely what crashing PgBouncer produces. An attacker who can do it once can do it in a loop.
PostgreSQL has had pre-authentication bugs too; it is not better designed against this class of problem. The difference is that a PostgreSQL fix arrives in a scheduled minor release that you already have a process for applying. PgBouncer has no such process, which is the actual subject of this post.
Why nobody patched it
PgBouncer is installed once and then forgotten, because it works. It is not on the PostgreSQL minor-release train; it ships when it ships (1.25.0 last November, 1.25.1 in December, 1.25.2 in May, and now 1.26.0), and it is in nobody’s quarterly “apply the Postgres minor” ritual. The distro package is routinely a version or three behind. The sidecar container in the Kubernetes manifest was built from whatever tag was current when someone wrote the manifest, and nobody has looked at it since. If a managed provider runs PgBouncer for you, you probably do not know what version it is, and you should ask.
A seven-year-old unauthenticated crash in the authentication path is what that neglect buys you.
What to do
Upgrade to 1.26.0. That is the whole recommendation.
If you cannot do it today: set max_packet_size well below 1073741824, which closes the hang, and put the listener behind a firewall rule that admits only application hosts, which is the only mitigation on offer for the crash. Then upgrade anyway.
Two things to know before you do. The deprecated online restart (-R, the “takeover” mode) is gone in 1.26.0, along with SHOW FDS and SUSPEND. If your deployment procedure depends on -R, the replacement is a rolling restart with so_reuseport, and you want that worked out before you are doing it under duress. Also, 1.26.0 now tracks every parameter PostgreSQL reports by default, which means search_path (on PostgreSQL 18) and default_transaction_read_only (on 14 and later) no longer leak between clients in transaction pooling mode. That is a fix, not a regression, but if some application was accidentally relying on the leak, you are about to find out.
The release codename is “Ignore all previous search_path.” The maintainers, at least, have a sense of humor about the year they are having.