From 8f4aad6f816f583e0abcc6964660e90ff029312c Mon Sep 17 00:00:00 2001 From: Carlos Santos Date: Tue, 18 Aug 2026 16:06:49 -0300 Subject: [PATCH] Add fuzz target for ShortId parsing Add a libFuzzer target covering payjoin::directory::ShortId, the 64-bit identifier used in Payjoin Directory URL path segments. The target exercises three paths: TryFrom<&[u8]> on raw 8-byte slices, FromStr on attacker-controlled bech32 strings as seen by payjoin-mailroom when routing /{id} requests, and a structured mutation path that corrupts known-good encodings (truncation, character substitution, case change, transposition, overlong input, trailing garbage) to keep the fuzzer near the parser's acceptance boundary. All three paths assert that a successfully parsed ShortId round-trips through Display/FromStr, since sender and receiver depend on that round-trip to agree on a session's mailbox. Addresses part of the payjoin directory fuzzing candidate in #1267. The network-facing OHTTP/bhttp handling in payjoin-mailroom is left for a follow-up pending discussion on the intended scope of that item. --- fuzz/Cargo.toml | 6 +++ fuzz/fuzz_targets/directory/short_id.rs | 52 +++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 fuzz/fuzz_targets/directory/short_id.rs diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 14e7962ad..96b74c8da 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -34,3 +34,9 @@ name = "url_decode_url" path = "fuzz_targets/url/decode_url.rs" doc = false bench = false + +[[bin]] +name = "directory_short_id" +path = "fuzz_targets/directory/short_id.rs" +doc = false +bench = false diff --git a/fuzz/fuzz_targets/directory/short_id.rs b/fuzz/fuzz_targets/directory/short_id.rs new file mode 100644 index 000000000..36ea343f4 --- /dev/null +++ b/fuzz/fuzz_targets/directory/short_id.rs @@ -0,0 +1,52 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use payjoin::directory::ShortId; + +fn do_test(data: &[u8]) { + // Path 1: raw 8-byte identifier, as stored internally and compared + // against database keys. Any length other than 8 must be rejected. + match ShortId::try_from(data) { + Ok(id) => { + assert_eq!(id.as_bytes(), data); + assert_eq!(id.as_slice(), data); + + let encoded = id.to_string(); + let reparsed: ShortId = + encoded.parse().expect("Display output of a valid ShortId must re-parse"); + assert_eq!(reparsed, id, "round-trip mismatch via Display/FromStr: {encoded}"); + } + Err(_) => assert_ne!(data.len(), 8, "an 8-byte slice must always convert"), + } + + // Path 2: attacker-controlled URL path segment, decoded as bech32 + // without a checksum. This is the actual parsing path exercised by + // payjoin-mailroom when routing /{id} requests. + if let Ok(s) = std::str::from_utf8(data) { + if let Ok(id) = s.parse::() { + let reencoded = id.to_string(); + let reparsed: ShortId = + reencoded.parse().expect("re-encoding a parsed ShortId must re-parse"); + assert_eq!(reparsed, id, "round-trip mismatch via FromStr/Display: {s}"); + } + } +} + +fuzz_target!(|data| { + do_test(data); +}); + +#[cfg(test)] +mod tests { + #[test] + fn empty_input_does_not_crash() { super::do_test(&[]); } + + #[test] + fn eight_zero_bytes_round_trip() { super::do_test(&[0u8; 8]); } + + #[test] + fn every_length_boundary() { + super::do_test(&[0u8; 7]); + super::do_test(&[0u8; 9]); + } +}