Back to Blog

"The column was encrypted": pgcrypto was storing cleartext and nobody noticed

"The column was encrypted": pgcrypto was storing cleartext and nobody noticed

On 13 August PostgreSQL shipped 18.6, 17.11, 16.15, 15.19 and 14.24, closing 28 vulnerabilities and over 110 bugs at once. Twelve of the twenty-eight are memory overflows, and another five are type confusions, with near-interchangeable titles. One looks like none of them. CVE-2026-14663 says that, under certain conditions, pgcrypto was writing cleartext into the column you believed was encrypted. No error, no warning, nothing in the log.

The 18.6 release notes tell it without anaesthetic: "Previously, if OpenSSL rejected the requested cipher (for example, because it is running in FIPS mode, or the legacy provider hasn't been loaded), pgcrypto failed to notice the failure and simply XOR'd the non-encrypted block with the plaintext, rendering the 'encryption' trivially breakable." So: pgcrypto asked OpenSSL for a cipher, OpenSSL said no, pgcrypto never read the answer and carried on. What ended up in the bytea was not ciphertext: it was your data with a layer of XOR over a block that was never encrypted.

The CVE page closes the loop with a sentence that admits no kind reading: the flaw "allows a user to recover cleartext, via direct observation of the faulty ciphertext." By looking at the stored value. And there is a second part, in case the first felt mild: "If the application accepts encrypted data as input, decryption will succeed even with the wrong key." The key had stopped mattering. The affected functions are the eight PGP ones: pgp_sym_encrypt, pgp_pub_encrypt, their decryption counterparts and the four _bytea variants.

Before anyone panics: the default is fine

This does not happen to everyone, and it is worth saying that first. The default cipher for pgp_sym_encrypt is aes128, and AES is available in any modern OpenSSL. The ciphers that trigger the flaw are the old ones: the release notes name blowfish/bf, twofish, cast5 and 3des. PostgreSQL scores it 6.5 on CVSS, and the number has its logic: both conditions have to line up. If your application has never written the cipher-algo option, this is almost certainly not about you. Almost: those options only govern what you encrypt. "Decrypt functions get the parameters from the PGP data", says the documentation. If your application decrypts PGP messages arriving from elsewhere, the cipher was chosen by whoever created them, and no grep of your repository reaches that far.

The uncomfortable part comes next. Nobody writes cipher-algo=bf in 2026. It was written more than a decade ago, copied from an example that was reasonable at the time, and it is still there because the column read back fine and nobody had a reason to look again. The places that string turns up are usually the same ones: integrations with ancient GnuPG, importers for another vendor's files, stored procedures that survived three migrations.

The day it broke was not the day it was written

The code that asks for Blowfish may have been encrypting correctly for years. What changed is not the application: it is the machine underneath. The pgcrypto documentation warns about it in one line: "When compiled against OpenSSL 3.0.0 and later versions, the legacy provider must be activated in the openssl.cnf configuration file in order to use older ciphers like DES or Blowfish." OpenSSL 3 moved Blowfish and CAST5 to the legacy provider, which does not load itself; 3DES stays in the default provider and disappears through another door, FIPS mode. Two different roads to the same place.

Put the two pieces together and you get a scene we have watched many times under other names: someone migrates the database server to a newer distribution on a Saturday morning. The application is not touched. The tests pass, because the tests encrypt and decrypt against the same server — encrypting badly and decrypting badly, perfectly consistently. Nothing falls over, nothing shows up in monitoring, nobody opens a ticket. And from that Saturday on, whatever goes into that column can be read. We already wrote here about a control that failed open; this one does not even fail: it returns OK.

The good news: from now on, it breaks

The fix, authored by Daniel Gustafsson, does the only sensible thing: it turns the silence into noise. "By default, pgcrypto will now fail to decrypt any messages that were affected in this way." Translated: you upgrade the server and, if you had the problem, the application stops reading that column. On a Monday morning, with a ticket attached. That ticket is the cheapest detector you will get this month, and it ships inside the package: if nothing stops decrypting after the upgrade, you did not have the flaw. If something does, you now know where to start looking.

The emergency exit and its small print

To recover the data left in that state, PostgreSQL added a new option to pgp_sym_decrypt and pgp_pub_decrypt: ignore-cipher-failure=1, which restores the previous behaviour so you can strip the faulty wrapper and re-encrypt with a modern algorithm. The documentation opens it with the word "Dangerous!" and adds the sentence worth reading slowly: "Such faulty messages do not require the correct decryption key when ignore-cipher-failure is enabled, so there is no guarantee that the decrypted plaintext actually originated from a holder of the key." Getting that data out does not require the key. Which is another way of saying what we said above: it was not encrypted.

And there is an ordering constraint that is easy to miss: "It is important however that the behavior of OpenSSL be the same as it was when the faulty messages were created: if the set of unsupported algorithms is not the same, this approach will not work." The rescue has to happen where the damage happened, on the same OpenSSL. If the plan was "we upgrade the server, move it to the new box, and then, calmly, look into that column thing", the plan is backwards. Rescue and re-encrypt with aes256 first; move afterwards.

And it is worth keeping two cases apart that look alike from outside. One is the message that was encrypted badly, and that is what this option fixes. The other is the message that was encrypted correctly, with a cipher your OpenSSL no longer offers; there the documentation is blunt: ignore-cipher-failure "is not necessary or helpful for that scenario", and the right move is to enable the missing provider. If the option is used anyway and the correctly encrypted message happens to pass PGP's integrity checks, "that result is coincidental and does not make the recovered plaintext trustworthy". It can hand you something that looks right and is not.

Which version you are really running

There are two traps here, and both affect how you check whether you are current. The first is on the official CVE page itself, which says: "Versions before PostgreSQL 18.5, 17.11, 16.15, 15.19, and 14.24 are affected", and lists 18.5 as the fixed version. 18.5 does not exist. The announcement from the same day explains it: "This release skips PostgreSQL 18 versions from PostgreSQL 18.4 to 18.6. 18.5 was not shipped due to a regression." If your inventory greps for the number on the CVE page, it will never find it. For the 18 branch, the number that matters is 18.6.

The second trap is where your packages come from. We checked today, 20 August in the afternoon, against the public repositories, and it is not the same answer:

  • PGDG (apt.postgresql.org), for bookworm and trixie: already serving the fixed versions of all five branches — 14.24, 15.19, 16.15, 17.11 and 18.6 (on trixie, 17.11-1.pgdg13+2).
  • Debian 13 (trixie), main repository: postgresql-17 is still at 17.10-0+deb13u1, which Debian's security tracker flags as vulnerable. The fix travels through the security channel: 17.11-0+deb13u1, released as DSA-6438-1. If that machine has no trixie-security line in its sources.list, it still has the flaw today.
  • Debian 12 (bookworm): same story with postgresql-1515.18 in main, 15.19-0+deb12u1 in security, as DLA-4740-1.
  • If you are still on PostgreSQL 13 (bullseye), the tracker lists it as vulnerable with no fixed version. There is no patch to apply there: there is a migration to plan. And if you are on the 14 branch, write this down now: PostgreSQL 14 stops receiving fixes on 12 November 2026.

And the document that says it was encrypted

If your record of processing activities, your security policy or your last answer to a customer questionnaire contains a line saying "sensitive data is stored encrypted in the database", that line may have stopped being true the day that machine changed OpenSSL. Article 34(3)(a) of the GDPR lets you skip notifying the individuals when the measures applied rendered the data unintelligible to anyone not authorised to read it, and it names encryption as an example. An XOR that comes undone without a key does not fit there comfortably.

We are not lawyers and that call is not ours to make. What is ours is the layer underneath: whoever has to decide needs to know which cipher the application asked for, which OpenSSL was on that machine, and from what date. All three can be reconstructed — the code, the openssl.cnf, the date of the operating system change — and they reconstruct far better today, calmly, than inside a case file with a seventy-two-hour clock on it.

What we would do this week

  • Grep for the string, not the CVE. grep -ri "cipher-algo" across the code, the stored procedures and the import jobs. If it does not appear, the matter is closed and it took ten minutes.
  • Look at the server, not the application. SELECT version(); checked against 18.6 / 17.11 / 16.15 / 15.19 / 14.24, plus a look at whether that machine's openssl.cnf loads the legacy provider. Those two answers together tell you whether the flaw was possible there.
  • Let the patch be the auditor. Upgrade a copy of the database first and read the affected columns. Whatever stops decrypting is the work list, with the caveat above: it may be a badly encrypted value or a correctly encrypted one whose cipher is no longer loaded, and they are not handled the same way.
  • Rescue before you move. If ignore-cipher-failure is needed, do it on the original machine with its OpenSSL. Re-encrypt with aes256, and write down who, when and which rows: that is what will be asked for later.
  • And while you are there, ask about the key. pgcrypto works inside the server, and its documentation says so plainly: "all the data and passwords move between pgcrypto and client applications in clear text", which is why you must "[t]rust both system and database administrator". If that column holds what we suspect it holds, the cipher was never the only question. From the same drawer: the data dashboard that was also the keyring, and the file that claimed to be a PNG.

The odd thing about this vulnerability is that it does not need anyone to attack you in order to hurt you. There is no exploitation campaign, no threat actor named after an animal. There is a column that may have spent years storing in the clear exactly what was put there so it could not be read, and exactly one person in the world can find out: the one who sits down and looks. The new version helps in the only way software can help here — it finally complains.

Sources (verified on 20 August 2026): the date, the released versions, the 28 vulnerabilities and over 110 bugs, the sentence about 18.5 not shipping and the PostgreSQL 14 end-of-life date, from the official announcement of 13 August 2026. The description of the flaw, the affected functions, the wrong-key sentence and the "before 18.5, 17.11, 16.15, 15.19, and 14.24" wording, from the CVE-2026-14663 page. The XOR explanation, the list of ciphers that trigger it, the authorship by Daniel Gustafsson and the new decryption behaviour, from the 18.6 release notes. The default cipher, the warning about OpenSSL 3's legacy provider, the "Dangerous!" note on ignore-cipher-failure, the sentence about the key not being needed, the requirement that OpenSSL behave the same way, and the quoted security limitations, from the pgcrypto documentation. The per-distribution status, DSA-6438-1 and DLA-4740-1, from the Debian security tracker. Ours are today's queries against the public indexes of apt.postgresql.org (trixie-pgdg and bookworm-pgdg); they are reproducible and return the same answer from anywhere. Ours too is the reading about the operating-system migration scenario, declared as a reading in the text. Quotes are left in English, their original language, so they can be verified word for word.

Do you know which cipher your application asks for?

We keep business databases and applications running that have been in production for years, with the boring part written down: which version runs, which repository it comes from, and what depends on what. If this week involves opening the box on an encrypted column nobody has looked at in a long time, that is exactly the work we do in data and applications, and the part about deciding what gets documented and what gets notified, in compliance and continuity.

Talk to everyWAN

Tags:

Share:

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