Published: 2026-09-01

CVE-2026-14669: The PostgreSQL to_char Bug to Patch Now

On August 13, 2026 PostgreSQL shipped 18.6, 17.11, 16.15, 15.19, and 14.24 and closed 28 security holes at once — the largest single security batch in the project's history. The one making the rounds on r/PostgreSQL is CVE-2026-14669: a heap buffer overflow in to_char() that any logged-in user can turn into arbitrary code execution as the OS account running your database. It scores CVSS 8.8, and a public proof-of-concept exists. If you run PostgreSQL, the short version is: patch, and read on for how to tell whether you're already exposed.

The one-paragraph answer

Upgrade any PostgreSQL 14–18 server to 14.24 / 15.19 / 16.15 / 17.11 / 18.6 (or later). It's a binary-swap minor release — no dump/reload. The vulnerable path is to_char() on a timestamp with a TZ/tz format code, reachable by any authenticated user, so “we don't expose to_char to end users” is not a mitigation if untrusted parties can log in at all. Check your server's running version, not your client's — a patched GUI in front of an unpatched server is still an unpatched server.

What was in the August 13 release

This was not a routine point release. The batch fixed 28 CVEs and over 110 bugs. For context on how unusual that is: PostgreSQL closed 7 CVEs in all of 2025; 2026 has produced roughly 44 so far. Seventeen of the 28 in this release score 8.0 or higher, and nine are “executes arbitrary code” class. A sample of the arbitrary-code-execution ones, to show it wasn't a single soft spot:

CVEWhereCVSS
CVE-2026-14669to_char() timezone abbreviation heap overflow8.8
CVE-2026-14664Regexp engine heap buffer overflow8.8
CVE-2026-14662tsvector/tsquery undersized allocation via integer wraparound8.8
CVE-2026-14676pg_stat_statements heap buffer overflow8.8
CVE-2026-15741SQL injection via EXTRACT argument deparse8.8
CVE-2026-19385pg_dump heap buffer overflow8.8

CVE-2026-14669 gets the attention because its trigger is trivially within reach of any user and it lives in a function almost every application calls. The rest of this post is about that one, but the takeaway generalizes: this release is not optional.

What CVE-2026-14669 actually is

The official description is precise and worth reading literally:

Heap buffer overflow in PostgreSQL to_char(timestamptz) allows the party choosing the timezone to execute arbitrary code as the operating system user running the database, via a long POSIX timezone abbreviation.

To unpack that, you have to know one quirk of the POSIX time-zone format. When you set a session time zone, you can name a real zone (SET TIME ZONE 'America/New_York') — or you can hand PostgreSQL a raw POSIX spec and invent your own abbreviation, which the angle-bracket syntax lets be arbitrarily long:

-- a made-up 16-character abbreviation, offset UTC-10
SET TIME ZONE '<ABCDEFGHIJKLMNOP>-10';

That abbreviation is now attacker-controlled text of attacker-controlled length. The problem is what to_char() does with it. When you format a timestamp with the TZ (uppercase) or tz (lowercase) code, the abbreviation is copied verbatim into the output. We checked the source: PostgreSQL sizes the whole output buffer from the format string's length — fmt_len × 12 + 1 bytes — which gives the two-character TZ token a budget of just 24 bytes. The pre-fix code then did an unbounded copy of the session abbreviation into that space:

// src/backend/utils/adt/formatting.c, DCH_to_char() — before the fix
case DCH_TZ:
    if (tmtcTzn(in))
    {
        strcpy(s, tmtcTzn(in));   // no length check
        s += strlen(s);
    }
    break;

An abbreviation longer than the token's budget writes past the end of the heap allocation. That is the whole bug: user-controlled data, user-controlled length, a fixed destination, and a missing bounds check. The fix (commit 3d724bf4) adds exactly the guard that was missing to both the TZ and tz paths:

// after the fix
if (strlen(p) <= n->key->len * DCH_MAX_ITEM_SIZ)
    strcpy(s, p);
else
    ereport(ERROR,
            (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
             errmsg("time zone format value too long")));

On a patched server, an oversized abbreviation now raises a clean time zone format value too long error instead of corrupting memory.

We confirmed the user-controlled path on a live server

Because this is an engine-specific behavior, we don't take the mechanism on faith — we ran it. Against an unpatched PostgreSQL 16.4 test server, as an ordinary login role, a made-up abbreviation flows straight through to_char() into the result, unmodified:

demo=> SET TIME ZONE '<ABCDEFGHIJKLMNOP>-10';
SET
demo=> SELECT to_char(now(), 'TZ');
     to_char
------------------
 ABCDEFGHIJKLMNOP
(1 row)

Sixteen characters is safely inside the 24-byte budget, so this is a benign observation, not an exploit — but it is the exact path the CVE abuses. Nothing here required elevated privileges: the role we used was a plain login account. Push the abbreviation past the buffer's budget on an unpatched server and you're in overflow territory. We deliberately did not do that, and neither should you against anything you rely on — a real trigger corrupts server memory and can crash the backend. The point of the safe version is only to show how short the distance is between “a user set their time zone” and “a user reached the vulnerable copy.”

Security analyses rate the bug reliably exploitable in practice — a published proof-of-concept chains the overflow through heap grooming to defeat ASLR and reach code execution as the postgres OS user. We're not going to reproduce any of that here; the mechanism above is the part that matters for deciding to patch, and the patch closes it completely.

Am I exposed?

Assume yes if all of these hold, which is the common case:

  • The server runs PostgreSQL 14.0 through 18.4 (or 17.10-and-earlier, 16.14-and-earlier, 15.18-and-earlier, 14.23-and-earlier). Everything before the August 13 releases is affected.
  • Untrusted or semi-trusted parties can authenticate. The trigger needs a logged-in session, but nothing more — no superuser, no special grant. A low-privilege application account, a multi-tenant database, a shared analytics login, or a BI tool with SQL passthrough all qualify.

One exposure pattern raised in the discussion thread is worth calling out because it's so common: applications that take a user-selected time zone straight into a query — think a dashboard that lets each viewer pick their display zone, then formats timestamps server-side with to_char. That is exactly “the party choosing the timezone,” and it can put the attacker-controlled abbreviation on the vulnerable path without the attacker ever holding a raw SQL connection. If your app does this, treat it as reachable.

What does not save you: not calling to_char in your own code. When someone can log in directly, they supply both the format code and the time zone themselves — they don't need your application to call it for them. Network-level isolation helps (fewer parties who can log in) but is a mitigation, not a fix — the only fix is the patched binary.

Check your server version in ten seconds

The number that decides your exposure is the running server's build. Ask the server directly:

SELECT version();
-- or just the number:
SHOW server_version;

Read the third component. 16.15 or higher on the 16 line is patched; 16.14 or lower is not. The same rule applies down each major version. Don't infer this from your client, your driver, or the package you think is installed — a connection pooler, a stale container image, or a managed instance that hasn't taken its maintenance window can all mean the server answering your queries is older than you assume.

If you use Jam SQL Studio, the running server version is shown in the connection info for each connection, so you can eyeball it per server without typing anything — useful when you're auditing a dozen connections and want to know which ones are still on a vulnerable build. It's a desktop client, though, so treat it as a fast way to read the version and confirm the fix landed, not as protection: no client can shield an unpatched server, because the vulnerable code runs inside the server process.

How to upgrade (and the post-upgrade steps that aren't about this CVE)

These are minor releases, so the disk format is unchanged and there's no pg_upgrade or dump/reload:

  1. Install the new package/binaries for your platform (14.24 / 15.19 / 16.15 / 17.11 / 18.6).
  2. Stop PostgreSQL, swap the binaries, restart.
  3. Apply the post-update steps below if they apply to you.

Three items in the release notes need manual action after updating — they're unrelated to CVE-2026-14669, but since you're doing the upgrade, don't miss them:

  • Parallel GIN index builds could leave a table's reltuples set to a bogus value (even Infinity or NaN), which quietly stops autovacuum/autoanalyze from touching it. Run ANALYZE on any affected table. The release notes include a query to find them.
  • btree_gist indexes on float4/float8/bit/bit varying columns that might contain NaN should be reindexed (REINDEX INDEX …) — a NaN-handling and sort-order fix.
  • ltree indexes over values with more than ~14,653 labels could have been corrupted by an integer overflow; reindex those too.

If you're on managed PostgreSQL

Amazon RDS/Aurora, Google Cloud SQL, and Azure Database for PostgreSQL don't inherit the community release on day one. Each provider rebuilds the minor version and rolls it out on its own schedule — typically days to a few weeks later — and many require you to opt into the new minor version during a maintenance window rather than applying it silently. Commenters on the thread flagged exactly this wait for Azure PaaS and Cloud SQL: you're gated on the provider publishing the build. So: run SELECT version() against the actual instance, check your provider's release notes for when the 14.24/…/18.6-equivalent build is available, and schedule the maintenance window. Until then, tighten who can connect — that reduces the set of parties who can reach the bug, even though it doesn't remove it.

Supabase is a useful specific case, since it came up in the thread. It does not track upstream automatically: on a paid project you stay on whatever version you're on until someone clicks Upgrade project in the project's settings, which takes the project offline, provisions a new instance, and moves the data across — and free projects only jump to the latest minor when a paused project is restored. (Supabase runs even minor upgrades through that new-instance flow, which is why it looks heavier than the plain binary swap a self-managed minor upgrade needs.) The practical takeaway is the same everywhere managed: read the actual Postgres version off the dashboard or with SELECT version(), don't assume the platform quietly tracked the fix.

Why so many PostgreSQL CVEs in 2026?

Going from 7 CVEs in a year to 44 doesn't mean PostgreSQL got worse — the affected functions (to_char, the regex engine, tsvector) have been stable for years. It means the codebase is finally getting the depth of fuzzing and security-researcher attention that other large C projects have had for a while, and old latent bugs are surfacing in bulk. That's healthy in the long run. In the short run it means minor releases now carry real security weight and shouldn't be deferred.

One deadline worth marking: PostgreSQL 14 reaches end of life on November 12, 2026. 14.24 is one of its last security releases. If you're still on 14, this upgrade buys you a fix now, but you should be planning the move to a newer major version before the fixes stop coming — the discovery rate isn't slowing down.

Quick answers

Q: Which PostgreSQL versions fix CVE-2026-14669?

A: 18.6, 17.11, 16.15, 15.19, and 14.24 (all August 13, 2026), plus 19 Beta 3. Everything earlier in the 14–18 lines is affected. There's no 18.5 to land on — it was skipped for a regression, so 18 goes 18.4 → 18.6.

Q: Who can trigger it?

A: Any authenticated user, with no special privilege. Setting a long POSIX time-zone abbreviation and calling to_char() with a TZ/tz code are both ordinary user actions. CVSS 8.8; impact is code execution as the OS account running PostgreSQL.

Q: How do I check my server version?

A: SELECT version(); or SHOW server_version; against the server. It's the running server's build that matters, not the client or driver. In Jam SQL Studio the server version shows in each connection's info dialog.

Q: Does upgrading need a dump and reload?

A: No — it's a minor release. Stop, swap binaries, restart. Separately, this batch has optional post-upgrade steps (an ANALYZE for parallel-GIN reltuples, a REINDEX for some btree_gist and ltree indexes) that aren't about this CVE.

Q: Is my cloud PostgreSQL already patched?

A: Not automatically on release day. Providers roll the new minor version out on their own schedule and often require a maintenance-window opt-in. Confirm with SELECT version() and your provider's release notes.

The takeaway

CVE-2026-14669 is a textbook missing-bounds-check: user-controlled text, user-controlled length, a 24-byte destination, and — until this release — no check between them. It's remotely reachable by anyone who can log in, it scores 8.8, and there's a public PoC. The fix is a one-line guard on the server; there is nothing to change in your queries and nothing a client can do for you. Read your server's real version, upgrade to the August 13 build for your major line, and don't let the “it's only a minor release” framing talk you out of it — this batch closed 28 holes, and this is one of the ones people are already exploiting.

See Every Server's Version at a Glance

Jam SQL Studio shows the running PostgreSQL, MySQL, SQL Server, and Oracle version in each connection's info dialog — a fast way to audit which servers are still on a vulnerable build. Free for personal use.

Related