Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions fuzz/fuzz_targets/verify.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#![no_main]
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
let _ = bcrypt::hash(&data, 4);
fuzz_target!(|data: (&[u8], &str)| {
// Exercise the hash-string parser by feeding both arbitrary
// password bytes and arbitrary &str inputs to verify().
// The &str input is what reaches split_hash; this is the
// surface that #62 (and its regression) lived in.
let _ = bcrypt::verify(data.0, data.1);
});
34 changes: 31 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,12 @@ fn _hash_password(
/// cost, salt and hash
#[cfg(any(feature = "alloc", feature = "std"))]
fn split_hash(hash: &str) -> BcryptResult<HashParts> {
// A valid bcrypt hash is always exactly 60 bytes:
if hash.len() != 60 {
// A valid bcrypt hash is always exactly 60 ASCII bytes. Rejecting
// non-ASCII up front avoids panics from `&str` slicing through the
// middle of a multi-byte UTF-8 character (regression of #62).
if hash.len() != 60 || !hash.is_ascii() {
return Err(BcryptError::InvalidHash(
"the hash format is malformed; expected 60 bytes",
"the hash format is malformed; expected 60 ASCII bytes",
));
}

Expand Down Expand Up @@ -748,4 +750,30 @@ mod tests {
"2a$$$0$OOOOOOOOOOOOOOOOOOOOO£OOOOOOOOOOOOOOOOOOOOOOOOOOOOOO",
);
}

#[test]
fn verify_rejects_multibyte_utf8_in_hash() {
// Constructed so byte position 22 falls inside the multi-byte
// sequence for '£' (0xC2 0xA3). Before this fix, this hash would
// panic in str::slice_error_fail when split_hash sliced
// &salt_and_hash[..22]. After: returns InvalidHash, like every
// other malformed input. Regression test for #62.
let hash = "$2b$04$aaaaaaaaaaaaaaaaaaaaa£aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
assert_eq!(hash.len(), 60); // sanity: byte length is still 60
assert!(matches!(
verify(&b"password"[..], hash),
Err(BcryptError::InvalidHash(_))
));
}

#[test]
fn split_hash_rejects_non_ascii() {
// Direct parser-level test of the invariant: any non-ASCII byte
// anywhere in a 60-byte hash string is rejected up front.
let hash = "$2b$04$aaaaaaaaaaaaaaaaaaaaa£aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
assert!(matches!(
split_hash(hash),
Err(BcryptError::InvalidHash(_))
));
}
}