From 728d0540949070eb965d8b843d2f62ba3e02c716 Mon Sep 17 00:00:00 2001 From: Andrey Rachitskiy Date: Mon, 3 Aug 2026 19:15:15 +0500 Subject: [PATCH] pgcrypto: Reject crypt() results invalid in database encoding crypt() copies a fixed number of caller-supplied salt bytes into its text result. When a multibyte character straddles that cut, the result contains a truncated sequence that is not valid in the database encoding, yet can still be stored. Validate the result with pg_verifymbstr(), as already done for pgp_*_decrypt_text. A regress case based on the report is included. Bug: #19600 Author: Andrey Rachitskiy Reported-by: Michael Malis Discussion: https://postgr.es/m/19600-e5fb479f50f5022d@postgresql.org --- contrib/pgcrypto/Makefile | 2 +- contrib/pgcrypto/expected/crypt-utf8.out | 8 ++++++++ contrib/pgcrypto/expected/crypt-utf8_1.out | 4 ++++ contrib/pgcrypto/meson.build | 1 + contrib/pgcrypto/pgcrypto.c | 4 ++++ contrib/pgcrypto/sql/crypt-utf8.sql | 7 +++++++ 6 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 contrib/pgcrypto/expected/crypt-utf8.out create mode 100644 contrib/pgcrypto/expected/crypt-utf8_1.out create mode 100644 contrib/pgcrypto/sql/crypt-utf8.sql diff --git a/contrib/pgcrypto/Makefile b/contrib/pgcrypto/Makefile index 17d2b0c5ed1..ca9f99a1b16 100644 --- a/contrib/pgcrypto/Makefile +++ b/contrib/pgcrypto/Makefile @@ -42,7 +42,7 @@ PGFILEDESC = "pgcrypto - cryptographic functions" REGRESS = init md5 sha1 hmac-md5 hmac-sha1 blowfish rijndael \ sha2 des 3des cast5 \ - crypt-des crypt-md5 crypt-blowfish crypt-xdes \ + crypt-des crypt-md5 crypt-blowfish crypt-xdes crypt-utf8 \ pgp-armor pgp-decrypt pgp-encrypt pgp-encrypt-md5 $(CF_PGP_TESTS) \ pgp-pubkey-decrypt pgp-pubkey-encrypt pgp-pubkey-session \ pgp-info crypt-shacrypt diff --git a/contrib/pgcrypto/expected/crypt-utf8.out b/contrib/pgcrypto/expected/crypt-utf8.out new file mode 100644 index 00000000000..7ce1b570ef9 --- /dev/null +++ b/contrib/pgcrypto/expected/crypt-utf8.out @@ -0,0 +1,8 @@ +/* Needs UTF8; skip otherwise (truncated multibyte salt copy). */ +SELECT getdatabaseencoding() <> 'UTF8' AS skip_test \gset +\if :skip_test +\quit +\endif +-- Salt bytes e282ac41 (euro then A). DES copies only e282, which is invalid UTF8. +SELECT crypt('password', convert_from(decode('e282ac41', 'hex'), 'utf8')); +ERROR: invalid byte sequence for encoding "UTF8": 0xe2 0x82 0x55 diff --git a/contrib/pgcrypto/expected/crypt-utf8_1.out b/contrib/pgcrypto/expected/crypt-utf8_1.out new file mode 100644 index 00000000000..5375d85fef6 --- /dev/null +++ b/contrib/pgcrypto/expected/crypt-utf8_1.out @@ -0,0 +1,4 @@ +/* Needs UTF8; skip otherwise (truncated multibyte salt copy). */ +SELECT getdatabaseencoding() <> 'UTF8' AS skip_test \gset +\if :skip_test +\quit diff --git a/contrib/pgcrypto/meson.build b/contrib/pgcrypto/meson.build index 4f255c8cb05..678a403baa1 100644 --- a/contrib/pgcrypto/meson.build +++ b/contrib/pgcrypto/meson.build @@ -46,6 +46,7 @@ pgcrypto_regress = [ 'crypt-md5', 'crypt-blowfish', 'crypt-xdes', + 'crypt-utf8', 'pgp-armor', 'pgp-decrypt', 'pgp-encrypt', diff --git a/contrib/pgcrypto/pgcrypto.c b/contrib/pgcrypto/pgcrypto.c index 9ecbbd2e2f8..8f822d683d1 100644 --- a/contrib/pgcrypto/pgcrypto.c +++ b/contrib/pgcrypto/pgcrypto.c @@ -33,6 +33,7 @@ #include +#include "mb/pg_wchar.h" #include "parser/scansup.h" #include "pgcrypto.h" #include "px-crypt.h" @@ -239,6 +240,9 @@ pg_crypt(PG_FUNCTION_ARGS) pfree(resbuf); + /* Ensure text result is valid in the database encoding. */ + pg_verifymbstr(VARDATA_ANY(res), VARSIZE_ANY_EXHDR(res), false); + PG_FREE_IF_COPY(arg0, 0); PG_FREE_IF_COPY(arg1, 1); diff --git a/contrib/pgcrypto/sql/crypt-utf8.sql b/contrib/pgcrypto/sql/crypt-utf8.sql new file mode 100644 index 00000000000..b6a45aabbe5 --- /dev/null +++ b/contrib/pgcrypto/sql/crypt-utf8.sql @@ -0,0 +1,7 @@ +/* Needs UTF8; skip otherwise (truncated multibyte salt copy). */ +SELECT getdatabaseencoding() <> 'UTF8' AS skip_test \gset +\if :skip_test +\quit +\endif +-- Salt bytes e282ac41 (euro then A). DES copies only e282, which is invalid UTF8. +SELECT crypt('password', convert_from(decode('e282ac41', 'hex'), 'utf8')); -- 2.53.0