bech32: check for invalid/unnecessary trailing bits.
What changed, and why it matters
This commit tightens how Core Lightning decodes bech32 strings—the format used for things like Bitcoin addresses and BOLT 12 offers. Previously, extra padding bits at the end of a bech32 string were silently thrown away, even if they were non-zero or unnecessarily long. Now the decoder rejects those cases. The change is driven by a new BOLT test vector that requires stricter decoding, and it could prevent subtle malleability or parsing-confusion issues where two different-looking bech32 strings decode to the same data.
Treat this as a correctness and likely low-grade security hardening fix. Review any other call sites of bech32_pull_bits() or similar decoders in the codebase to ensure they enforce the same rules, and consider adding regression tests for the new BOLT test vectors. Users processing untrusted bech32/BOLT 12 strings should upgrade to avoid accepting malformed encodings.
Security signals we found
Stricter input validation in a widely-used encoding decoder
Rejection of non-zero trailing padding bits that were previously silently discarded
Rejection of superfluous 5-bit groups that expand the encoded string without carrying data
Potential malleability reduction for BOLT 12 offers and other bech32 payloads
Change triggered by an updated BOLT specification test vector
Evidence from the diff
The patch changes bech32_pull_bits() from a void function to a bool function and adds two validation rules: (1) if the requested bit count leaves 5 or more padding bits, it means a superfluous 5-bit group was present and decoding fails; (2) any remaining padding bits (1–4 bits) must all be zero. Callers in from_bech32_charset() and encrypted_decode() now check the return value and fail cleanly. The Makefile is also updated to point to a newer BOLT RFC revision that includes the new test vector.
Changed components
common/bech32_util.ccommon/bech32_util.hplugins/offers.cMakefile (BOLT version pin)Inspect captured patch +24 / −6
diff --git a/Makefile b/Makefile
index a511542d..db14d23e 100644
--- a/Makefile
+++ b/Makefile
@@ -33,7 +33,7 @@ CCANDIR := ccan
# Where we keep the BOLT RFCs
BOLTDIR := ../bolts/
-DEFAULT_BOLTVERSION := b9a1206eb2d7fe7c535e3399c212f289e88b2898
+DEFAULT_BOLTVERSION := 7153bed9705d7493065d9b818d25b282ef0a7c5e
# Can be overridden on cmdline.
BOLTVERSION := $(DEFAULT_BOLTVERSION)
diff --git a/common/bech32_util.c b/common/bech32_util.c
index b72777e7..3722a19c 100644
--- a/common/bech32_util.c
+++ b/common/bech32_util.c
@@ -31,12 +31,16 @@ static u8 get_u5_bit(const u5 *src, size_t bitoff)
return ((src[bitoff / 5] >> (4 - (bitoff % 5))) & 1);
}
-void bech32_pull_bits(u8 **data, const u5 *src, size_t nbits)
+bool bech32_pull_bits(u8 **data, const u5 *src, size_t nbits)
{
size_t i;
size_t data_len = tal_count(*data);
+ size_t pad = nbits % 8;
+
+ /* More than 4 padding bits means a superfluous u5 group was added. */
+ if (pad >= 5)
+ return false;
- /* We discard trailing bits. */
for (i = 0; i + 8 <= nbits; i += 8) {
tal_resize(data, data_len+1);
(*data)[data_len] = 0;
@@ -46,6 +50,13 @@ void bech32_pull_bits(u8 **data, const u5 *src, size_t nbits)
}
data_len++;
}
+
+ /* Padding bits must all be zero. */
+ for (size_t b = 0; b < pad; b++) {
+ if (get_u5_bit(src, i + b))
+ return false;
+ }
+ return true;
}
/* Returns a char, tracks case. */
@@ -95,7 +106,10 @@ bool from_bech32_charset(const tal_t *ctx,
goto fail;
*data = tal_arr(ctx, u8, 0);
- bech32_pull_bits(data, u5data, tal_bytelen(u5data) * 5);
+ if (!bech32_pull_bits(data, u5data, tal_bytelen(u5data) * 5)) {
+ tal_free(*data);
+ goto fail;
+ }
tal_free(u5data);
return true;
diff --git a/common/bech32_util.h b/common/bech32_util.h
index f2857bde..fbd71d04 100644
--- a/common/bech32_util.h
+++ b/common/bech32_util.h
@@ -12,8 +12,9 @@ void bech32_push_bits(u5 **data, const void *src, size_t nbits);
/**
* Push the bytes in src in 8 bit format onto the end of data.
+ * Returns false if padding bits are non-zero or exceed 4 bits.
*/
-void bech32_pull_bits(u8 **data, const u5 *src, size_t nbits);
+bool bech32_pull_bits(u8 **data, const u5 *src, size_t nbits);
/**
* Checksumless bech32 routines.
diff --git a/plugins/offers.c b/plugins/offers.c
index 436a32f4..6c049334 100644
--- a/plugins/offers.c
+++ b/plugins/offers.c
@@ -538,7 +538,10 @@ static u8 *encrypted_decode(const tal_t *ctx, const char *str, char **fail) {
goto fail;
}
u8 *data8bit = tal_arr(data, u8, 0);
- bech32_pull_bits(&data8bit, data, datalen*5);
+ if (!bech32_pull_bits(&data8bit, data, datalen*5)) {
+ *fail = tal_fmt(ctx, "invalid bech32 padding");
+ goto fail;
+ }
return data8bit;
fail:
Why this scored 51/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.