Check metadata length for bolt11 invoices
What changed, and why it matters
This commit fixes a missing length check when creating BOLT11 Lightning invoices. Before the fix, a user or attacker could supply payment metadata longer than the 639-byte BOLT11 limit, producing an invoice that violates the protocol and may fail to be parsed or routed correctly. The patch now rejects oversized metadata during invoice creation, similar to the existing check for descriptions.
Review whether any other optional tagged fields (route hints, fallback addresses, expiry, etc.) have similar missing length checks, and ensure all BOLT11 length limits are enforced consistently during invoice creation. Consider adding fuzz or property tests for invoice builder inputs.
Security signals we found
Missing input validation on BOLT11 invoice metadata length
Protocol compliance violation: tagged field exceeds 639-byte BOLT11 limit
New error variant CreationError::PaymentMetadataTooLong introduced
Unit test added for oversized metadata rejection
Evidence from the diff
The change adds a constant MAX_TAGGED_FIELD_DATA_BYTES (639 bytes) and enforces it in InvoiceBuilder::optional_payment_metadata. Previously, any Vec
Changed components
lightning-invoice/src/lib.rsInvoiceBuilder::optional_payment_metadataCreationError enumInspect captured patch +19 / −3
diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs
index 3826adc..2dfd752 100644
--- a/lightning-invoice/src/lib.rs
+++ b/lightning-invoice/src/lib.rs
@@ -159,6 +159,10 @@ pub const DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA: u64 = 18;
/// consistency is more important.
pub const MAX_LENGTH: usize = 7089;
+/// The maximum length of a tagged field in a BOLT11 invoice. This is 1023 * 5 bits (i.e., 639
+/// bytes).
+pub const MAX_TAGGED_FIELD_DATA_BYTES: usize = 639;
+
/// The [`bech32::Bech32`] checksum algorithm, with extended max length suitable
/// for BOLT11 invoices.
pub enum Bolt11Bech32 {}
@@ -886,7 +890,11 @@ impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool>
pub fn optional_payment_metadata(
mut self, payment_metadata: Vec<u8>,
) -> InvoiceBuilder<D, H, T, C, S, tb::True> {
- self.tagged_fields.push(TaggedField::PaymentMetadata(payment_metadata));
+ if payment_metadata.len() > MAX_TAGGED_FIELD_DATA_BYTES {
+ self.error = Some(CreationError::PaymentMetadataTooLong);
+ } else {
+ self.tagged_fields.push(TaggedField::PaymentMetadata(payment_metadata));
+ }
let mut found_features = false;
for field in self.tagged_fields.iter_mut() {
if let TaggedField::Features(f) = field {
@@ -1676,12 +1684,12 @@ impl TaggedField {
}
impl Description {
- /// Creates a new `Description` if `description` is at most 1023 * 5 bits (i.e., 639 bytes)
+ /// Creates a new `Description` if `description` is at most [`MAX_TAGGED_FIELD_DATA_BYTES`]
/// long, and returns [`CreationError::DescriptionTooLong`] otherwise.
///
/// Please note that single characters may use more than one byte due to UTF8 encoding.
pub fn new(description: String) -> Result<Description, CreationError> {
- if description.len() > 639 {
+ if description.len() > MAX_TAGGED_FIELD_DATA_BYTES {
Err(CreationError::DescriptionTooLong)
} else {
Ok(Description(UntrustedString(description)))
@@ -1798,6 +1806,9 @@ pub enum CreationError {
/// The supplied description string was longer than 639 __bytes__ (see [`Description::new`])
DescriptionTooLong,
+ /// The supplied payment metadata was longer than 639 __bytes__
+ PaymentMetadataTooLong,
+
/// The specified route has too many hops and can't be encoded
RouteTooLong,
@@ -1820,6 +1831,7 @@ impl Display for CreationError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
CreationError::DescriptionTooLong => f.write_str("The supplied description string was longer than 639 bytes"),
+ CreationError::PaymentMetadataTooLong => f.write_str("The supplied payment metadata was longer than 639 bytes"),
CreationError::RouteTooLong => f.write_str("The specified route has too many hops and can't be encoded"),
CreationError::TimestampOutOfBounds => f.write_str("The Unix timestamp of the supplied date is less than zero or greater than 35-bits"),
CreationError::InvalidAmount => f.write_str("The supplied millisatoshi amount was greater than the total bitcoin supply"),
@@ -2276,6 +2288,10 @@ mod test {
let long_desc_res = builder.clone().description(too_long_string).build_raw();
assert_eq!(long_desc_res, Err(CreationError::DescriptionTooLong));
+ let long_metadata_res =
+ builder.clone().description("Test".into()).payment_metadata(vec![0u8; 640]).build_raw();
+ assert_eq!(long_metadata_res, Err(CreationError::PaymentMetadataTooLong));
+
let route_hop = RouteHintHop {
src_node_id: PublicKey::from_slice(
&[
Why this scored 60/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.