Reject pre-epoch `LSPSDateTime` at parse time
What changed, and why it matters
This commit fixes a remote denial-of-service bug in rust-lightning's LSPS (Lightning Service Provider Specification) code. An attacker could send a specially crafted date string from before 1970 (like "1900-01-01T00:00:00Z") in certain peer messages. The date would be accepted, and later when the software checked whether it had expired, it would panic and crash the LSP thread. The fix rejects any pre-1970 date during parsing, including when reading JSON from peers, so the dangerous value can never be created.
Treat this as a security fix and include it in the next maintenance release. Users running LSP nodes with LSPS1/LSPS2 services should upgrade promptly because the panic is remotely triggerable without authentication. No immediate downstream mitigation is available other than patching or disabling LSPS services.
Security signals we found
Remote-triggerable panic (DoS) via peer-controlled input
Integer conversion panic: i64 negative timestamp coerced to u64 with .expect()
Input validation bypass: serde transparent deserialization skipped custom parser
Fix funnels both FromStr and serde deserialization through a single parser
Regression test added for both parse paths
Evidence from the diff
LSPSDateTime wrapped chrono::DateTime
Changed components
lightning-liquidity/src/lsps0/ser.rsLSPSDateTimeLSPS2 opening_fee_params.valid_until handlingLSPS1 expiry fields handlingprune_pending_requests sweepsInspect captured patch +29 / −3
diff --git a/lightning-liquidity/src/lsps0/ser.rs b/lightning-liquidity/src/lsps0/ser.rs
index d28bba7..1ac900b 100644
--- a/lightning-liquidity/src/lsps0/ser.rs
+++ b/lightning-liquidity/src/lsps0/ser.rs
@@ -234,7 +234,7 @@ impl Readable for LSPSRequestId {
}
/// An object representing datetimes as described in bLIP-50 / LSPS0.
-#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
+#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(transparent)]
pub struct LSPSDateTime(pub chrono::DateTime<chrono::Utc>);
@@ -275,8 +275,23 @@ impl LSPSDateTime {
impl FromStr for LSPSDateTime {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
- let datetime = chrono::DateTime::parse_from_rfc3339(s).map_err(|_| ())?;
- Ok(Self(datetime.into()))
+ let datetime: chrono::DateTime<chrono::Utc> =
+ chrono::DateTime::parse_from_rfc3339(s).map_err(|_| ())?.into();
+ // Reject pre-epoch datetimes here so peer-controlled `valid_until` /
+ // `expires_at` fields can never produce an `LSPSDateTime` with a negative
+ // UNIX timestamp, which would otherwise panic the `i64 -> u64` cast in
+ // `is_past`.
+ if datetime.timestamp() < 0 {
+ return Err(());
+ }
+ Ok(Self(datetime))
+ }
+}
+
+impl<'de> Deserialize<'de> for LSPSDateTime {
+ fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+ let s = String::deserialize(deserializer)?;
+ Self::from_str(&s).map_err(|()| de::Error::custom("invalid LSPSDateTime"))
}
}
@@ -996,4 +1011,15 @@ mod tests {
assert_eq!(later.duration_since(&earlier), Duration::from_secs(60));
assert_eq!(earlier.duration_since(&later), Duration::ZERO);
}
+
+ #[test]
+ fn is_past_handles_pre_epoch_datetime() {
+ // A peer-controlled RFC3339 datetime before 1970 must be rejected at parse
+ // time, so it can never reach `is_past` (or any other consumer) and panic.
+ assert!(LSPSDateTime::from_str("1900-01-01T00:00:00Z").is_err());
+
+ // JSON deserialization (the path peer messages take) must reject it too.
+ let json = "\"1900-01-01T00:00:00Z\"";
+ assert!(serde_json::from_str::<LSPSDateTime>(json).is_err());
+ }
}
Why this scored 76/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.