What changed#
We are rolling out the PostgreSQL 15.19 / 17.11 minor release (from 15.14 / 17.6). It bundles five upstream security cycles and closes 44 CVEs. Four changes may require action, depending on how you use your database:
- ltree indexes may need reindexing — a case-folding fix on multibyte/ICU databases, plus an integer-overflow fix for very deep
ltreevalues (any encoding). - pgcrypto stops decrypting legacy-cipher PGP data (CVE-2026-14663) — data encrypted with
cipher-algo=bf/blowfish/cast5was effectively stored unencrypted and fails to decrypt by default after the upgrade. - btree_gist indexes on float columns that may contain
NaNneed reindexing. - Custom operators with non-built-in selectivity estimators (CVE-2026-2004) — recreating them (dump/restore, branching) now requires superuser.
If none of the detection queries below return rows for your project, no action is needed.
Why we made this change#
This is an upstream PostgreSQL minor release.
Staying current closes 44 CVEs (including several rated High) and picks up correctness fixes in the ltree and btree_gist extensions.
Who is affected#
1. ltree indexes#
Case-folding (multibyte/ICU)#
After upgrading, indexes on ltree columns built under the previous version can silently return incomplete results on databases using a multibyte encoding (such as UTF-8) or a non-libc collation provider (ICU or builtin).
Check whether your database is affected:
_10select_10 pg_encoding_to_char(encoding) as encoding,_10 pg_encoding_max_length(encoding) as max_bytes_per_char, -- 1 = single-byte, >1 = multibyte_10 datlocprovider as collation_provider, -- 'c' libc, 'i' icu, 'b' builtin_10 (pg_encoding_max_length(encoding) > 1 or datlocprovider != 'c') as reindex_required_10from pg_database_10where datname = current_database();
If reindex_required is true, find the affected indexes:
_14select distinct_14 n.nspname as schema_name,_14 cls.relname as table_name,_14 ic.relname as index_name_14from pg_index idx_14 join pg_class ic on idx.indexrelid = ic.oid_14 join pg_class cls on idx.indrelid = cls.oid_14 join pg_namespace n on ic.relnamespace = n.oid_14 join lateral unnest(idx.indclass::oid[]) with ordinality as k(opclass, pos) on true_14 join pg_opclass oc on oc.oid = k.opclass_14 join pg_type ty on ty.oid = oc.opcintype_14where_14 k.pos <= idx.indnkeyatts -- key columns only, excludes INCLUDE_14 and ty.typname in ('ltree', '_ltree');
Integer overflow (any encoding)#
This release also fixes an integer overflow in ltree comparisons: values with more than about 14,653 labels could compare incorrectly, which can corrupt B-tree indexes built over them, regardless of your database encoding.
This query lists only the B-tree indexes that actually contain such values (an empty result means no action is needed):
_35select s.schema_name || '.' || s.index_name as index_to_reindex_35from (_35 select_35 n.nspname as schema_name,_35 c.relname as table_name,_35 ic.relname as index_name,_35 min(pg_get_expr(i.indpred, i.indrelid)) as pred, -- partial-index predicate, if any_35 string_agg('nlevel(' || pg_get_indexdef(i.indexrelid, k.pos::int, true) || ') > 14653', ' or ') as keys_cond_35 from pg_index i_35 join pg_class ic on ic.oid = i.indexrelid_35 join pg_class c on c.oid = i.indrelid_35 join pg_namespace n on n.oid = c.relnamespace_35 join pg_am am on am.oid = ic.relam_35 join lateral generate_series(1, i.indnkeyatts) as k(pos) on true_35 join pg_attribute ia on ia.attrelid = i.indexrelid and ia.attnum = k.pos_35 join pg_type t on t.oid = ia.atttypid_35 where am.amname = 'btree'_35 and t.typname = 'ltree'_35 and n.nspname not in ('pg_catalog', 'information_schema')_35 group by n.nspname, c.relname, ic.relname_35) s_35where (xpath(_35 '/row/cnt/text()',_35 query_to_xml(_35 format('select count(*) as cnt from %I.%I where %s(%s)',_35 s.schema_name,_35 s.table_name,_35 case when s.pred is not null then '(' || s.pred || ') and ' else '' end,_35 s.keys_cond),_35 false,_35 true,_35 ''_35 )_35 ))[1]::text::bigint > 0_35order by 1;
2. pgcrypto legacy ciphers (CVE-2026-14663)#
If you call pgp_sym_encrypt/pgp_pub_encrypt (or their _bytea variants) with cipher-algo=bf, cipher-algo=blowfish, or cipher-algo=cast5, that data was effectively stored unencrypted — decryption succeeds even with the wrong key.
The default cipher (AES) and 3des are not affected.
If you never passed a cipher-algo option, no action is needed.
To find affected rows, scan each stored value with a deliberately wrong passphrase: properly encrypted values raise an error, while affected values decrypt successfully even with the wrong key.
Run the helper and the scan in the same session (pg_temp functions are session-scoped):
_14create function pg_temp.affected_by_cve_2026_14663(msg bytea)_14returns boolean_14language plpgsql as $$_14begin_14 perform pgp_sym_decrypt_bytea(msg, 'deliberately-wrong-key');_14 return true;_14exception when others then_14 return false;_14end $$;_14_14select <id_column>_14from <your_table>_14where <your_encrypted_column> is not null_14 and pg_temp.affected_by_cve_2026_14663(<your_encrypted_column>);
Any rows returned hold affected values.
The scan covers symmetric (pgp_sym_*) messages.
The wrong-key probe does not apply to public-key (pgp_pub_*) messages.
If pgp_pub_encrypt was used with an affected cipher-algo, treat those values as affected and re-encrypt them the same way using pgp_pub_decrypt and pgp_pub_encrypt with the key pair.
3. btree_gist indexes on float columns#
Indexes built under the previous version can return wrong results for rows containing NaN until reindexed.
Find btree_gist indexes on float columns:
_16select distinct_16 n.nspname as schema_name,_16 cls.relname as table_name,_16 ic.relname as index_name_16from pg_index idx_16 join pg_class ic on idx.indexrelid = ic.oid_16 join pg_am am on ic.relam = am.oid_16 join pg_class cls on idx.indrelid = cls.oid_16 join pg_namespace n on ic.relnamespace = n.oid_16 join lateral unnest(idx.indclass::oid[]) with ordinality as k(opclass, pos) on true_16 join pg_opclass oc on oc.oid = k.opclass_16 join pg_type ty on ty.oid = oc.opcintype_16where_16 am.amname = 'gist'_16 and k.pos <= idx.indnkeyatts_16 and ty.typname in ('float4', 'float8');
You are affected only if indexes are returned and those columns may contain NaN values.
4. Custom operators (CVE-2026-2004)#
Only superusers may now attach a non-built-in selectivity estimator to an operator. Existing operators keep working. Only re-creation (dump/restore, branching, major-version upgrade) fails.
Operators installed by extensions (PostGIS, intarray, etc.) are not affected. Check for affected operators:
_13select_13 n.nspname as schema, o.oprname as operator,_13 o.oprrest::regproc as restrict_estimator,_13 o.oprjoin::regproc as join_estimator_13from pg_operator o_13join pg_namespace n on o.oprnamespace = n.oid_13where n.nspname not in ('pg_catalog', 'information_schema')_13 and ((o.oprrest <> 0 and o.oprrest::oid >= 10000)_13 or (o.oprjoin <> 0 and o.oprjoin::oid >= 10000))_13 and not exists (_13 select 1 from pg_depend d_13 where d.classid = 'pg_operator'::regclass and d.objid = o.oid and d.deptype = 'e'_13 );
What happens if you take no action#
- ltree / btree_gist: affected index searches may silently return wrong or incomplete results - no error is raised.
- pgcrypto: decryption of affected messages fails by default after the upgrade (recoverable with the
ignore-cipher-failure=1decrypt option). The underlying data remains effectively unencrypted at rest until re-encrypted. - Custom operators: a future dump/restore, branch, or major-version upgrade fails to recreate the operator.
Migration steps#
- Run the detection queries above for each database.
- After upgrading, reindex each affected ltree/btree_gist index using its schema-qualified name —
REINDEX INDEX CONCURRENTLY <schema_name>.<index_name>;runs online with no downtime, but cannot run inside a transaction block. - For pgcrypto: re-encrypt affected values with a modern cipher (for example
cipher-algo=aes256), before the upgrade or after it usingignore-cipher-failure=1, and consider rotating secrets stored this way. - For custom operators: recreate without the
RESTRICT/JOINclause (or with a built-in estimator), or contact support and we'll assist.
Rollout timeline#
| Date | Milestone | User action |
|---|---|---|
| 2026-09-25 | Release announce (this entry) | Run detection queries |
| 2026-09-28 | New projects created on 15.19 / 17.11 | — |
| 2026-09-28 | Upgrade available in dashboard for existing projects | Upgrade, then reindex if affected |