feat: add validation for leading whitespace in BOLT 12 bech32 strings
What changed, and why it matters
This commit fixes a parsing bug in rust-lightning's handling of BOLT 12 payment offers. Previously, the library incorrectly accepted offers that began with whitespace characters when the offer string was split across multiple lines using '+' continuation characters. This violated the BOLT 12 specification and could cause rust-lightning to accept malformed offers that other Lightning implementations would reject. The fix adds explicit validation to reject any offer string that starts with whitespace.
Review whether any other BOLT 12 parsing edge cases differ from the specification and consider expanding differential fuzzing coverage against C-Lightning and other implementations. Ensure downstream users update to include this validation fix.
Security signals we found
Specification non-compliance in cryptographic/offer parsing
Cross-implementation differential fuzzing finding
Input validation weakness allowing malformed bech32 strings
Potential interoperability or denial-of-service vector via malformed BOLT 12 offers
Evidence from the diff
The change modifies lightning/src/offers/parse.rs to add a new InvalidLeadingWhitespace error variant and validate that bech32-encoded BOLT 12 strings do not contain whitespace in the first chunk (before any ‘+’ continuation character). Previously, the parser split on ‘+’ and trimmed/validated all chunks uniformly, which allowed leading whitespace in the initial segment to slip through. The fix separates first-chunk validation from continuation-chunk validation. A test case with a leading vertical tab character (\u{b}) is added.
Changed components
lightning/src/offers/parse.rsBOLT 12 offer parsingBech32 string validationInspect captured patch +25 / −1
diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs
index 38e69e2..8e8d01b 100644
--- a/lightning/src/offers/parse.rs
+++ b/lightning/src/offers/parse.rs
@@ -43,7 +43,20 @@ mod sealed {
// Offer encoding may be split by '+' followed by optional whitespace.
let encoded = match s.split('+').skip(1).next() {
Some(_) => {
- for chunk in s.split('+') {
+ let mut chunks = s.split('+');
+
+ // Check first chunk without trimming
+ if let Some(first_chunk) = chunks.next() {
+ if first_chunk.contains(char::is_whitespace) {
+ return Err(Bolt12ParseError::InvalidLeadingWhitespace);
+ }
+ if first_chunk.is_empty() {
+ return Err(Bolt12ParseError::InvalidContinuation);
+ }
+ }
+
+ // Check remaining chunks
+ for chunk in chunks {
let chunk = chunk.trim_start();
if chunk.is_empty() || chunk.contains(char::is_whitespace) {
return Err(Bolt12ParseError::InvalidContinuation);
@@ -123,6 +136,8 @@ pub enum Bolt12ParseError {
/// The bech32 encoding does not conform to the BOLT 12 requirements for continuing messages
/// across multiple parts (i.e., '+' followed by whitespace).
InvalidContinuation,
+ /// The bech32 string starts with whitespace, which violates BOLT 12 encoding requirements.
+ InvalidLeadingWhitespace,
/// The bech32 encoding's human-readable part does not match what was expected for the message
/// being parsed.
InvalidBech32Hrp,
@@ -322,6 +337,15 @@ mod tests {
}
}
+ #[test]
+ fn fails_parsing_bech32_encoded_offer_with_leading_whitespace() {
+ let encoded_offer = "\u{b}lno1pqpzwyq2p32x2um5ypmx2cm5dae8x93pqthvwfzadd7jejes8q9lhc4rvjxd022zv5l44g6qah+\u{b}\u{b}\u{b}\u{b}82ru5rdpnpj";
+ match encoded_offer.parse::<Offer>() {
+ Ok(_) => panic!("Valid offer: {}", encoded_offer),
+ Err(e) => assert_eq!(e, Bolt12ParseError::InvalidLeadingWhitespace),
+ }
+ }
+
#[test]
fn fails_parsing_bech32_encoded_offer_with_invalid_bech32_data() {
let encoded_offer = "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxo";
Why this scored 38/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.