Add cargo-fuzz targets for the untrusted-input parsers and the hardening report
envelope_json, manifest_decode, item_replay, secret_key_parse, totp_parse, url_match, with seeds. fuzz/README.md records the runs (and the server/desktop targets), the two bugs fixed in the previous commits, dependency-policy results, CI changes and the deploy check. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
//! kv-proto Envelope JSON decode: anything accepted must re-encode and decode to the same value.
|
||||
#![no_main]
|
||||
use kv_proto::Envelope;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
if let Ok(e) = serde_json::from_slice::<Envelope>(data) {
|
||||
let again = serde_json::to_vec(&e).expect("re-encode");
|
||||
let back: Envelope = serde_json::from_slice(&again).expect("decode own encoding");
|
||||
assert_eq!(back, e);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
//! Decrypted item plaintext: JSON parse, Item model parse, and the per-item replay checks and merge that
|
||||
//! run on every pulled record. Input: two plaintexts (held base, incoming) separated by a 0xFF byte.
|
||||
#![no_main]
|
||||
use kv_model::Item;
|
||||
use kv_sync::{merge::merge3, replay};
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use serde_json::Value;
|
||||
use uuid::Uuid;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let (a, b) = match data.iter().position(|&c| c == 0xff) {
|
||||
Some(i) => (&data[..i], &data[i + 1..]),
|
||||
None => (data, &data[..0]),
|
||||
};
|
||||
let Ok(incoming) = serde_json::from_slice::<Value>(a) else { return };
|
||||
let _ = serde_json::from_value::<Item>(incoming.clone());
|
||||
let id = Uuid::from_u128(1);
|
||||
let held: Option<Value> = serde_json::from_slice(b).ok();
|
||||
let base = match &held {
|
||||
Some(v) => replay::Base::Sealed(v),
|
||||
None => replay::Base::LegacyTombstone,
|
||||
};
|
||||
for deleted in [false, true] {
|
||||
let _ = replay::check_pulled(base, id, deleted, Some(&incoming));
|
||||
}
|
||||
// An identical record is never a replay.
|
||||
assert!(replay::check_successor(replay::Base::Sealed(&incoming), &incoming).is_ok());
|
||||
let next = replay::next_version(Some(&incoming), base);
|
||||
assert!(next > replay::seq_of(&incoming) || replay::seq_of(&incoming) == u64::MAX);
|
||||
if let Some(h) = &held {
|
||||
let _ = merge3(None, h, &incoming);
|
||||
let _ = merge3(Some(h), &incoming, h);
|
||||
let _ = replay::same_content(h, &incoming);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
//! kv-sync ManifestBody::decode (strict canonical CBOR): accepted input must be exactly its own encoding.
|
||||
#![no_main]
|
||||
use kv_sync::manifest::ManifestBody;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
if let Ok(body) = ManifestBody::decode(data) {
|
||||
assert_eq!(body.encode(), data, "non-canonical input accepted");
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
//! kv-crypto SecretKey::parse: never panics; an accepted key round-trips through its display form.
|
||||
#![no_main]
|
||||
use kv_crypto::secret_key::SecretKey;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let Ok(s) = std::str::from_utf8(data) else { return };
|
||||
if let Ok(k) = SecretKey::parse(s) {
|
||||
let again = SecretKey::parse(&k.to_display()).expect("parse own display form");
|
||||
assert_eq!(again.expose(), k.expose());
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
//! kv-model TOTP / otpauth:// parse, then code generation at fuzzed times.
|
||||
#![no_main]
|
||||
use kv_model::totp::Totp;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
if data.len() < 8 {
|
||||
return;
|
||||
}
|
||||
let t = u64::from_le_bytes(data[..8].try_into().unwrap());
|
||||
let Ok(s) = std::str::from_utf8(&data[8..]) else { return };
|
||||
if let Ok(totp) = Totp::parse(s) {
|
||||
let code = totp.code_at(t);
|
||||
assert_eq!(code.len(), totp.digits as usize);
|
||||
assert!(code.bytes().all(|c| c.is_ascii_digit()));
|
||||
let r = totp.remaining(t);
|
||||
assert!(r >= 1 && r <= totp.period);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
//! kv-model URL matching: never panics, a URL always matches itself at least at host level, and an
|
||||
//! https item never matches an http page.
|
||||
#![no_main]
|
||||
use kv_model::{match_url, MatchLevel};
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let Ok(s) = std::str::from_utf8(data) else { return };
|
||||
let (a, b) = s.split_once('\n').unwrap_or((s, ""));
|
||||
let m = match_url(a, b);
|
||||
if b.trim_start().to_ascii_lowercase().starts_with("http://")
|
||||
&& a.trim_start().to_ascii_lowercase().starts_with("https://")
|
||||
{
|
||||
assert_eq!(m, MatchLevel::None, "downgrade matched: {a:?} -> {b:?}");
|
||||
}
|
||||
let _ = match_url(a, a);
|
||||
});
|
||||
Reference in New Issue
Block a user