agora inbox for pgsql-bugs@postgresql.org
help / color / mirror / Atom feedBUG #19714: pgcrypto pgp_sym_encrypt accepts nonnumeric s2k-mode as mode 0
2+ messages / 2 participants
[nested] [flat]
* BUG #19714: pgcrypto pgp_sym_encrypt accepts nonnumeric s2k-mode as mode 0
@ 2026-09-22 16:05 PG Bug reporting form <noreply@postgresql.org>
2026-09-25 04:37 ` Re: BUG #19714: pgcrypto pgp_sym_encrypt accepts nonnumeric s2k-mode as mode 0 shihao zhong <zhong950419@gmail.com>
0 siblings, 1 reply; 2+ messages in thread
From: PG Bug reporting form @ 2026-09-22 16:05 UTC (permalink / raw)
To: pgsql-bugs@lists.postgresql.org; +Cc: imchifan@163.com
The following bug has been logged on the website:
Bug reference: 19714
Logged by: Qifan Liu
Email address: imchifan@163.com
PostgreSQL version: 18.6
Operating system: Linux on amd64
Description:
pgp_sym_encrypt accepts the malformed option s2k-mode=not_a_number and
produces usable ciphertext. The documented s2k-mode values are numeric modes
0, 1, and 3, so nonnumeric text should be rejected rather than silently
selecting mode 0. This can cause encryption to use a different string-to-key
mode than the caller specified. The impact is localized to pgcrypto option
validation.
Steps to reproduce
------------------
CREATE EXTENSION pgcrypto;
SELECT pgp_sym_decrypt(
pgp_sym_encrypt('payload',
'key',
's2k-mode=not_a_number'),
'key') = 'payload' AS malformed_s2k_mode_accepted;
Actual result
-------------
malformed_s2k_mode_accepted
-----------------------------
t
(1 row)
The malformed value is accepted, and the produced ciphertext decrypts
successfully.
Expected result
---------------
pgp_sym_encrypt should reject s2k-mode=not_a_number with an error because
s2k-mode accepts only the documented numeric values. It should not interpret
malformed text as mode 0 or produce ciphertext.
Additional information
----------------------
The issue was reproduced on PostgreSQL 20devel, PostgreSQL 18.6, and
PostgreSQL 17.11.
Inference: the behavior is consistent with numeric conversion that maps text
without a valid numeric prefix to zero before validating the resulting mode.
^ permalink raw reply [nested|flat] 2+ messages in thread
* Re: BUG #19714: pgcrypto pgp_sym_encrypt accepts nonnumeric s2k-mode as mode 0
2026-09-22 16:05 BUG #19714: pgcrypto pgp_sym_encrypt accepts nonnumeric s2k-mode as mode 0 PG Bug reporting form <noreply@postgresql.org>
@ 2026-09-25 04:37 ` shihao zhong <zhong950419@gmail.com>
0 siblings, 0 replies; 2+ messages in thread
From: shihao zhong @ 2026-09-25 04:37 UTC (permalink / raw)
To: imchifan@163.com; pgsql-bugs@lists.postgresql.org
Hi
All the integer PGP options are parsed with atoi(), so trailing junk
("s2k-mode=3x") and values that wrap around int ("s2k-mode=4294967299"
gives 3) get through too.
This is not a common case. It only happens when the caller writes a bad
option string, and *the caller could ask for mode 0 directly anyway,* so
it is not a security problem. The one case worth fixing is s2k-mode.
Junk there gives the unsalted mode 0, and decryption still works, so
nobody would notice. "s2k-mode=salted" is an easy mistake to make,
since the other S2K options take names.
0001 parses the values with strtoint() and raises the existing "Illegal
argument to function" error. 0002 adds tests and is optional.
This makes some inputs that work today fail, so I'd keep it to master.
I can do back-branch versions if a committer wants it back-patched.
Shihao
Attachments:
[application/octet-stream] v1-0002-pgcrypto-Add-tests-for-malformed-integer-PGP-opti.patch (3.0K, ../../CAGRkXqR5G=Z1U5-EE_LGqa9TGP082sJiBZM4JR39crbrzVZbXw@mail.gmail.com/3-v1-0002-pgcrypto-Add-tests-for-malformed-integer-PGP-opti.patch)
download | inline diff:
From 4d01289a53adea3b6c980e6a4b2328dd09e29e3b Mon Sep 17 00:00:00 2001
From: Shihao <zhong950419@gmail.com>
Date: Thu, 24 Sep 2026 23:58:52 -0400
Subject: [PATCH v1 2/2] pgcrypto: Add tests for malformed integer PGP option
values.
Check that a non-numeric value, trailing junk and an out of range value
are rejected. Without the parser fix, all three are silently accepted
and "s2k-mode=not_a_number" encrypts with S2K mode 0.
Discussion: https://postgr.es/m/19714-2c7439b39f73bde9@postgresql.org
---
contrib/pgcrypto/expected/pgp-encrypt.out | 7 +++++++
contrib/pgcrypto/expected/pgp-encrypt_1.out | 7 +++++++
contrib/pgcrypto/sql/pgp-encrypt.sql | 5 +++++
3 files changed, 19 insertions(+)
diff --git a/contrib/pgcrypto/expected/pgp-encrypt.out b/contrib/pgcrypto/expected/pgp-encrypt.out
index 50cd3f6daa0..a6ca7334079 100644
--- a/contrib/pgcrypto/expected/pgp-encrypt.out
+++ b/contrib/pgcrypto/expected/pgp-encrypt.out
@@ -120,6 +120,13 @@ NOTICE: pgp_decrypt: unexpected s2k_count: expected 65000000 got 65011712
Secret.
(1 row)
+-- integer options must be valid integers
+select pgp_sym_encrypt('Secret.', 'key', 's2k-mode=not_a_number');
+ERROR: Illegal argument to function
+select pgp_sym_encrypt('Secret.', 'key', 's2k-mode=3x');
+ERROR: Illegal argument to function
+select pgp_sym_encrypt('Secret.', 'key', 's2k-count=4294968320');
+ERROR: Illegal argument to function
-- s2k digest change
select pgp_sym_decrypt(
pgp_sym_encrypt('Secret.', 'key', 's2k-digest-algo=sha1'),
diff --git a/contrib/pgcrypto/expected/pgp-encrypt_1.out b/contrib/pgcrypto/expected/pgp-encrypt_1.out
index 36b1052809c..e828300f415 100644
--- a/contrib/pgcrypto/expected/pgp-encrypt_1.out
+++ b/contrib/pgcrypto/expected/pgp-encrypt_1.out
@@ -116,6 +116,13 @@ NOTICE: pgp_decrypt: unexpected s2k_count: expected 65000000 got 65011712
Secret.
(1 row)
+-- integer options must be valid integers
+select pgp_sym_encrypt('Secret.', 'key', 's2k-mode=not_a_number');
+ERROR: Illegal argument to function
+select pgp_sym_encrypt('Secret.', 'key', 's2k-mode=3x');
+ERROR: Illegal argument to function
+select pgp_sym_encrypt('Secret.', 'key', 's2k-count=4294968320');
+ERROR: Illegal argument to function
-- s2k digest change
select pgp_sym_decrypt(
pgp_sym_encrypt('Secret.', 'key', 's2k-digest-algo=sha1'),
diff --git a/contrib/pgcrypto/sql/pgp-encrypt.sql b/contrib/pgcrypto/sql/pgp-encrypt.sql
index f67329c2c30..e806f5761af 100644
--- a/contrib/pgcrypto/sql/pgp-encrypt.sql
+++ b/contrib/pgcrypto/sql/pgp-encrypt.sql
@@ -62,6 +62,11 @@ select pgp_sym_decrypt(
pgp_sym_encrypt('Secret.', 'key', 's2k-count=65000000'),
'key', 'expect-s2k-count=65000000');
+-- integer options must be valid integers
+select pgp_sym_encrypt('Secret.', 'key', 's2k-mode=not_a_number');
+select pgp_sym_encrypt('Secret.', 'key', 's2k-mode=3x');
+select pgp_sym_encrypt('Secret.', 'key', 's2k-count=4294968320');
+
-- s2k digest change
select pgp_sym_decrypt(
pgp_sym_encrypt('Secret.', 'key', 's2k-digest-algo=sha1'),
--
2.37.1 (Apple Git-137.1)
[application/octet-stream] v1-0001-pgcrypto-Reject-malformed-integer-values-in-PGP-o.patch (4.9K, ../../CAGRkXqR5G=Z1U5-EE_LGqa9TGP082sJiBZM4JR39crbrzVZbXw@mail.gmail.com/4-v1-0001-pgcrypto-Reject-malformed-integer-values-in-PGP-o.patch)
download | inline diff:
From 055fb4bf397b3ca7470b9f4e7d17cf8f2875bc92 Mon Sep 17 00:00:00 2001
From: Shihao <zhong950419@gmail.com>
Date: Thu, 24 Sep 2026 23:58:52 -0400
Subject: [PATCH v1 1/2] pgcrypto: Reject malformed integer values in PGP
options.
The PGP option parser used atoi() for integer options, so a value that
is not a number was silently taken as 0. Trailing junk and out of range
values were accepted too. For s2k-mode this is bad, since
"s2k-mode=foo" gives mode 0, which is S2K without salt. The other
integer options had the same problem.
Parse integer option values with strtoint() instead, and raise the same
"Illegal argument to function" error that we already raise for other
bad option values.
Reported-by: Qifan Liu <imchifan@163.com>
Discussion: https://postgr.es/m/19714-2c7439b39f73bde9@postgresql.org
---
contrib/pgcrypto/pgp-pgsql.c | 50 ++++++++++++++++++++++++------------
1 file changed, 34 insertions(+), 16 deletions(-)
diff --git a/contrib/pgcrypto/pgp-pgsql.c b/contrib/pgcrypto/pgp-pgsql.c
index 05ef45fbe4c..06797af89c4 100644
--- a/contrib/pgcrypto/pgp-pgsql.c
+++ b/contrib/pgcrypto/pgp-pgsql.c
@@ -164,6 +164,24 @@ show_debug(const char *msg)
ereport(NOTICE, (errmsg("dbg: %s", msg)));
}
+/*
+ * Parse the value of an integer option. Don't use atoi() here, as it would
+ * silently turn a value like "foo" into 0, which for s2k-mode means no salt.
+ */
+static int
+parse_int_arg(const char *val)
+{
+ char *endptr;
+ int result;
+
+ errno = 0;
+ result = strtoint(val, &endptr, 10);
+ if (errno != 0 || endptr == val || *endptr != '\0')
+ px_THROW_ERROR(PXE_ARGUMENT_ERROR);
+
+ return result;
+}
+
static int
set_arg(PGP_Context *ctx, char *key, char *val,
struct debug_expect *ex)
@@ -173,34 +191,34 @@ set_arg(PGP_Context *ctx, char *key, char *val,
if (strcmp(key, "cipher-algo") == 0)
res = pgp_set_cipher_algo(ctx, val);
else if (strcmp(key, "disable-mdc") == 0)
- res = pgp_disable_mdc(ctx, atoi(val));
+ res = pgp_disable_mdc(ctx, parse_int_arg(val));
else if (strcmp(key, "sess-key") == 0)
- res = pgp_set_sess_key(ctx, atoi(val));
+ res = pgp_set_sess_key(ctx, parse_int_arg(val));
else if (strcmp(key, "s2k-mode") == 0)
- res = pgp_set_s2k_mode(ctx, atoi(val));
+ res = pgp_set_s2k_mode(ctx, parse_int_arg(val));
else if (strcmp(key, "s2k-count") == 0)
- res = pgp_set_s2k_count(ctx, atoi(val));
+ res = pgp_set_s2k_count(ctx, parse_int_arg(val));
else if (strcmp(key, "s2k-digest-algo") == 0)
res = pgp_set_s2k_digest_algo(ctx, val);
else if (strcmp(key, "s2k-cipher-algo") == 0)
res = pgp_set_s2k_cipher_algo(ctx, val);
else if (strcmp(key, "compress-algo") == 0)
- res = pgp_set_compress_algo(ctx, atoi(val));
+ res = pgp_set_compress_algo(ctx, parse_int_arg(val));
else if (strcmp(key, "compress-level") == 0)
- res = pgp_set_compress_level(ctx, atoi(val));
+ res = pgp_set_compress_level(ctx, parse_int_arg(val));
else if (strcmp(key, "convert-crlf") == 0)
- res = pgp_set_convert_crlf(ctx, atoi(val));
+ res = pgp_set_convert_crlf(ctx, parse_int_arg(val));
else if (strcmp(key, "unicode-mode") == 0)
- res = pgp_set_unicode_mode(ctx, atoi(val));
+ res = pgp_set_unicode_mode(ctx, parse_int_arg(val));
else if (strcmp(key, "ignore-cipher-failure") == 0)
- res = pgp_set_ignore_cipher_failure(ctx, atoi(val));
+ res = pgp_set_ignore_cipher_failure(ctx, parse_int_arg(val));
/*
* The remaining options are for debugging/testing and are therefore not
* documented in the user-facing docs.
*/
else if (ex != NULL && strcmp(key, "debug") == 0)
- ex->debug = atoi(val);
+ ex->debug = parse_int_arg(val);
else if (ex != NULL && strcmp(key, "expect-cipher-algo") == 0)
{
ex->expect = 1;
@@ -209,22 +227,22 @@ set_arg(PGP_Context *ctx, char *key, char *val,
else if (ex != NULL && strcmp(key, "expect-disable-mdc") == 0)
{
ex->expect = 1;
- ex->disable_mdc = atoi(val);
+ ex->disable_mdc = parse_int_arg(val);
}
else if (ex != NULL && strcmp(key, "expect-sess-key") == 0)
{
ex->expect = 1;
- ex->use_sess_key = atoi(val);
+ ex->use_sess_key = parse_int_arg(val);
}
else if (ex != NULL && strcmp(key, "expect-s2k-mode") == 0)
{
ex->expect = 1;
- ex->s2k_mode = atoi(val);
+ ex->s2k_mode = parse_int_arg(val);
}
else if (ex != NULL && strcmp(key, "expect-s2k-count") == 0)
{
ex->expect = 1;
- ex->s2k_count = atoi(val);
+ ex->s2k_count = parse_int_arg(val);
}
else if (ex != NULL && strcmp(key, "expect-s2k-digest-algo") == 0)
{
@@ -239,12 +257,12 @@ set_arg(PGP_Context *ctx, char *key, char *val,
else if (ex != NULL && strcmp(key, "expect-compress-algo") == 0)
{
ex->expect = 1;
- ex->compress_algo = atoi(val);
+ ex->compress_algo = parse_int_arg(val);
}
else if (ex != NULL && strcmp(key, "expect-unicode-mode") == 0)
{
ex->expect = 1;
- ex->unicode_mode = atoi(val);
+ ex->unicode_mode = parse_int_arg(val);
}
else
res = PXE_ARGUMENT_ERROR;
--
2.37.1 (Apple Git-137.1)
^ permalink raw reply [nested|flat] 2+ messages in thread
end of thread, other threads:[~2026-09-25 04:37 UTC | newest]
Thread overview: 2+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-09-22 16:05 BUG #19714: pgcrypto pgp_sym_encrypt accepts nonnumeric s2k-mode as mode 0 PG Bug reporting form <noreply@postgresql.org>
2026-09-25 04:37 ` shihao zhong <zhong950419@gmail.com>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox