agora inbox for pgsql-bugs@postgresql.org
help / color / mirror / Atom feedBUG #19702: decode() accepts Base64 payload after terminal padding
2+ messages / 2 participants
[nested] [flat]
* BUG #19702: decode() accepts Base64 payload after terminal padding
@ 2026-09-19 11:15 PG Bug reporting form <noreply@postgresql.org>
0 siblings, 1 reply; 2+ messages in thread
From: PG Bug reporting form @ 2026-09-19 11:15 UTC (permalink / raw)
To: pgsql-bugs@lists.postgresql.org; +Cc: imchifan@163.com
The following bug has been logged on the website:
Bug reference: 19702
Logged by: Qifan Liu
Email address: imchifan@163.com
PostgreSQL version: 18.6
Operating system: Linux/amd64
Description:
decode() accepts Base64 alphabet characters after terminal '=' padding and
incorporates them into the decoded bytea value. Once terminal padding
completes a Base64 value, only ignorable whitespace may follow. Applications
relying on decode() to validate Base64 input may consequently process
malformed input as valid data.
Steps to reproduce
------------------
Run the following with psql:
\set ON_ERROR_STOP on
SELECT encode(decode('YQ==Yg==', 'base64'), 'hex') AS decoded_hex;
SELECT encode(decode('YQ==AAAA', 'base64'), 'hex') AS
decoded_hex_after_padding;
Actual result
-------------
decoded_hex
-------------
6162
(1 row)
decoded_hex_after_padding
---------------------------
6100
(1 row)
Expected result
---------------
Both decode() calls should reject their input with SQLSTATE 22023 because
Base64 alphabet characters occur after terminal '=' padding. They should not
silently decode the trailing payload.
Additional information
----------------------
The issue was reproduced on PostgreSQL 20devel, PostgreSQL 18.6, and
PostgreSQL 17.11.
^ permalink raw reply [nested|flat] 2+ messages in thread
* Re: BUG #19702: decode() accepts Base64 payload after terminal padding
@ 2026-09-19 14:59 shihao zhong <zhong950419@gmail.com>
parent: PG Bug reporting form <noreply@postgresql.org>
0 siblings, 0 replies; 2+ messages in thread
From: shihao zhong @ 2026-09-19 14:59 UTC (permalink / raw)
To: imchifan@163.com; pgsql-bugs@lists.postgresql.org
Hi Qifan,
Thanks for reporting that issue.
I can reproduce this on master. The decoder sets "end" at the first "="
and never looks at it again, so later data and later "=" all pass
0001 raises an error for anything but whitespace after the padding, the
same rule base32hex already has. 0002 adds tests. 0003 fixes the two
copies of this code, pg_b64_decode() in src/common and the armor decoder
in pgcrypto. dearmor() shows the same bug when the CRC matches.
This rejects input that used to pass, so I am not sure about the back
branches. I would leave that to the committer.
Thanks,
Shihao
Attachments:
[application/octet-stream] v1-0002-Add-tests-for-data-after-base64-padding.patch (2.7K, ../../CAGRkXqQ1oavtxsq8y7Be63ig=iXic+ogTU2UoH9FQMBcqYi6iQ@mail.gmail.com/3-v1-0002-Add-tests-for-data-after-base64-padding.patch)
download | inline diff:
From 5633f6de6f8286b8ff79ed4a8d25ddadfd542e51 Mon Sep 17 00:00:00 2001
From: Shihao <zhong950419@gmail.com>
Date: Sat, 19 Sep 2026 10:45:48 -0400
Subject: [PATCH v1 2/3] Add tests for data after base64 padding
Author: Shihao Zhong <zhong950419@gmail.com>
Discussion: https://postgr.es/m/19702-9ed4a131fcfadb9d@postgresql.org
---
src/test/regress/expected/strings.out | 18 ++++++++++++++++++
src/test/regress/sql/strings.sql | 7 +++++++
2 files changed, 25 insertions(+)
diff --git a/src/test/regress/expected/strings.out b/src/test/regress/expected/strings.out
index fa29abfd829..5cbcb40a267 100644
--- a/src/test/regress/expected/strings.out
+++ b/src/test/regress/expected/strings.out
@@ -2905,6 +2905,24 @@ SELECT decode(encode(('\x' || repeat('1234567890abcdef0001', 7))::bytea,
\x1234567890abcdef00011234567890abcdef00011234567890abcdef00011234567890abcdef00011234567890abcdef00011234567890abcdef00011234567890abcdef0001
(1 row)
+-- nothing but whitespace may follow base64 padding
+SELECT decode(E'YQ=\n= \n', 'base64'); -- OK
+ decode
+--------
+ \x61
+(1 row)
+
+SELECT decode('YQ==Yg==', 'base64'); -- error
+ERROR: invalid symbol "Y" found while decoding base64 sequence
+SELECT decode('YQ==AAAA', 'base64'); -- error
+ERROR: invalid symbol "A" found while decoding base64 sequence
+SELECT decode('YQ=a', 'base64'); -- error
+ERROR: invalid symbol "a" found while decoding base64 sequence
+SELECT decode('YQ======', 'base64'); -- error
+ERROR: unexpected "=" while decoding base64 sequence
+SELECT decode('YQ=', 'base64url'); -- error
+ERROR: invalid base64url end sequence
+HINT: Input data is missing padding, is truncated, or is otherwise corrupted.
SELECT encode('\x1234567890abcdef00', 'escape');
encode
-----------------------------
diff --git a/src/test/regress/sql/strings.sql b/src/test/regress/sql/strings.sql
index 7d9c7275a02..66e42c21700 100644
--- a/src/test/regress/sql/strings.sql
+++ b/src/test/regress/sql/strings.sql
@@ -986,6 +986,13 @@ SELECT decode('1234567890abcdef00', 'hex');
SELECT encode(('\x' || repeat('1234567890abcdef0001', 7))::bytea, 'base64');
SELECT decode(encode(('\x' || repeat('1234567890abcdef0001', 7))::bytea,
'base64'), 'base64');
+-- nothing but whitespace may follow base64 padding
+SELECT decode(E'YQ=\n= \n', 'base64'); -- OK
+SELECT decode('YQ==Yg==', 'base64'); -- error
+SELECT decode('YQ==AAAA', 'base64'); -- error
+SELECT decode('YQ=a', 'base64'); -- error
+SELECT decode('YQ======', 'base64'); -- error
+SELECT decode('YQ=', 'base64url'); -- error
SELECT encode('\x1234567890abcdef00', 'escape');
SELECT decode(encode('\x1234567890abcdef00', 'escape'), 'escape');
--
2.37.1 (Apple Git-137.1)
[application/octet-stream] v1-0001-Reject-data-after-padding-in-base64-decoding.patch (1.8K, ../../CAGRkXqQ1oavtxsq8y7Be63ig=iXic+ogTU2UoH9FQMBcqYi6iQ@mail.gmail.com/4-v1-0001-Reject-data-after-padding-in-base64-decoding.patch)
download | inline diff:
From b24021aea09f5e2fdaa8e68ce383f0f90693ff92 Mon Sep 17 00:00:00 2001
From: Shihao <zhong950419@gmail.com>
Date: Sat, 19 Sep 2026 10:45:13 -0400
Subject: [PATCH v1 1/3] Reject data after padding in base64 decoding
decode() kept reading after the "=" padding that ends a base64 value.
It took more data, and more "=" at any position, and cut each later
group short. So 'YQ==AAAA' gave \x6100 with no error. Raise an error
for anything but whitespace after the padding, as base32hex does.
Bug: #19702
Reported-by: Qifan Liu <imchifan@163.com>
Author: Shihao Zhong <zhong950419@gmail.com>
Discussion: https://postgr.es/m/19702-9ed4a131fcfadb9d@postgresql.org
---
src/backend/utils/adt/encode.c | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/src/backend/utils/adt/encode.c b/src/backend/utils/adt/encode.c
index b9d4f2811d7..7aa56466718 100644
--- a/src/backend/utils/adt/encode.c
+++ b/src/backend/utils/adt/encode.c
@@ -536,8 +536,8 @@ pg_base64_decode_internal(const char *src, size_t len, char *dst, bool url)
if (c == '=')
{
- /* end sequence */
- if (!end)
+ /* end sequence, after it only the second "=" of "==" is allowed */
+ if (!end || pos != 3)
{
if (pos == 2)
end = 1;
@@ -556,7 +556,8 @@ pg_base64_decode_internal(const char *src, size_t len, char *dst, bool url)
else
{
b = -1;
- if (c > 0 && c < 127)
+ /* no data is allowed after padding */
+ if (c > 0 && c < 127 && !end)
b = b64lookup[(unsigned char) c];
if (b < 0)
{
@@ -583,12 +584,12 @@ pg_base64_decode_internal(const char *src, size_t len, char *dst, bool url)
}
}
- if (url && pos == 2)
+ if (url && !end && pos == 2)
{
buf <<= 12;
*p++ = (buf >> 16) & 0xFF;
}
- else if (url && pos == 3)
+ else if (url && !end && pos == 3)
{
buf <<= 6;
*p++ = (buf >> 16) & 0xFF;
--
2.37.1 (Apple Git-137.1)
[application/octet-stream] v1-0003-Reject-data-after-base64-padding-in-other-decoder.patch (3.3K, ../../CAGRkXqQ1oavtxsq8y7Be63ig=iXic+ogTU2UoH9FQMBcqYi6iQ@mail.gmail.com/5-v1-0003-Reject-data-after-base64-padding-in-other-decoder.patch)
download | inline diff:
From 44a1eb291a4224ddfe43d8a423b82d79fa4cfd14 Mon Sep 17 00:00:00 2001
From: Shihao <zhong950419@gmail.com>
Date: Sat, 19 Sep 2026 10:47:19 -0400
Subject: [PATCH v1 3/3] Reject data after base64 padding in other decoders
pg_b64_decode() in src/common and the armor decoder in pgcrypto share
the logic fixed in the previous commit. dearmor() took an armor with
data after the padding when the CRC matched. Fix both the same way.
Author: Shihao Zhong <zhong950419@gmail.com>
Discussion: https://postgr.es/m/19702-9ed4a131fcfadb9d@postgresql.org
---
contrib/pgcrypto/expected/pgp-armor.out | 10 ++++++++++
contrib/pgcrypto/pgp-armor.c | 8 ++++++--
contrib/pgcrypto/sql/pgp-armor.sql | 10 ++++++++++
src/common/base64.c | 7 ++++---
4 files changed, 30 insertions(+), 5 deletions(-)
diff --git a/contrib/pgcrypto/expected/pgp-armor.out b/contrib/pgcrypto/expected/pgp-armor.out
index 0f5ff461805..b3662a642f0 100644
--- a/contrib/pgcrypto/expected/pgp-armor.out
+++ b/contrib/pgcrypto/expected/pgp-armor.out
@@ -100,6 +100,16 @@ em9va2E=
-----END PGP MESSAGE-----
');
ERROR: Corrupt ascii-armor
+-- corrupt (data after padding)
+select dearmor('
+-----BEGIN PGP MESSAGE-----
+
+YQ==
+AAAA
+=Pr0b
+-----END PGP MESSAGE-----
+');
+ERROR: Corrupt ascii-armor
-- corrupt (no space after the colon)
select * from pgp_armor_headers('
-----BEGIN PGP MESSAGE-----
diff --git a/contrib/pgcrypto/pgp-armor.c b/contrib/pgcrypto/pgp-armor.c
index bfc90af063d..ddd40d72a6e 100644
--- a/contrib/pgcrypto/pgp-armor.c
+++ b/contrib/pgcrypto/pgp-armor.c
@@ -119,9 +119,9 @@ pg_base64_decode(const uint8 *src, unsigned len, uint8 *dst)
else if (c == '=')
{
/*
- * end sequence
+ * end sequence, after it only the second "=" of "==" is allowed
*/
- if (!end)
+ if (!end || pos != 3)
{
if (pos == 2)
end = 1;
@@ -137,6 +137,10 @@ pg_base64_decode(const uint8 *src, unsigned len, uint8 *dst)
else
return PXE_PGP_CORRUPT_ARMOR;
+ /* no data is allowed after padding */
+ if (end && c != '=')
+ return PXE_PGP_CORRUPT_ARMOR;
+
/*
* add it to buffer
*/
diff --git a/contrib/pgcrypto/sql/pgp-armor.sql b/contrib/pgcrypto/sql/pgp-armor.sql
index 736b54206f0..d4a4743b4c8 100644
--- a/contrib/pgcrypto/sql/pgp-armor.sql
+++ b/contrib/pgcrypto/sql/pgp-armor.sql
@@ -55,6 +55,16 @@ em9va2E=
-----END PGP MESSAGE-----
');
+-- corrupt (data after padding)
+select dearmor('
+-----BEGIN PGP MESSAGE-----
+
+YQ==
+AAAA
+=Pr0b
+-----END PGP MESSAGE-----
+');
+
-- corrupt (no space after the colon)
select * from pgp_armor_headers('
-----BEGIN PGP MESSAGE-----
diff --git a/src/common/base64.c b/src/common/base64.c
index aaaefc2921a..9cd92cacb59 100644
--- a/src/common/base64.c
+++ b/src/common/base64.c
@@ -134,8 +134,8 @@ pg_b64_decode(const char *src, int len, uint8 *dst, int dstlen)
if (c == '=')
{
- /* end sequence */
- if (!end)
+ /* end sequence, after it only the second "=" of "==" is allowed */
+ if (!end || pos != 3)
{
if (pos == 2)
end = 1;
@@ -155,7 +155,8 @@ pg_b64_decode(const char *src, int len, uint8 *dst, int dstlen)
else
{
b = -1;
- if (c > 0 && c < 127)
+ /* no data is allowed after padding */
+ if (c > 0 && c < 127 && !end)
b = b64lookup[(unsigned char) c];
if (b < 0)
{
--
2.37.1 (Apple Git-137.1)
^ permalink raw reply [nested|flat] 2+ messages in thread
end of thread, other threads:[~2026-09-19 14:59 UTC | newest]
Thread overview: 2+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-09-19 11:15 BUG #19702: decode() accepts Base64 payload after terminal padding PG Bug reporting form <noreply@postgresql.org>
2026-09-19 14:59 ` 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