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 |