| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849 |
- //! Fuzz the SSRF URL gate and address classifier (`protocol::ssrf`).
- //!
- //! [`validate_fetch_url`] is pure: an accepted URL is always `https`, has a
- //! non-empty host, and carries no credentials. The classifier half maps the
- //! first 16 input bytes to an [`IpAddr`] and calls [`is_blocked_ip`]; an
- //! IPv4-mapped address must be judged exactly like its embedded IPv4 address.
- //! [`fetch_guarded_bytes_with`] is deliberately not called: it is async, opens
- //! a socket, and is non-hermetic.
- #![no_main]
- use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
- use d_oauth::protocol::ssrf::{is_blocked_ip, validate_fetch_url};
- use libfuzzer_sys::fuzz_target;
- fuzz_target!(|data: &[u8]| {
- if let Ok(raw) = core::str::from_utf8(data)
- && let Ok(url) = validate_fetch_url(raw)
- {
- assert_eq!(url.scheme(), "https", "accepted URL is not https");
- assert!(
- url.host_str().is_some_and(|host| !host.is_empty()),
- "accepted URL has no host"
- );
- assert!(url.username().is_empty(), "accepted URL kept a username");
- assert!(url.password().is_none(), "accepted URL kept a password");
- }
- if data.len() < 16 {
- return;
- }
- let mut octets = [0u8; 16];
- octets.copy_from_slice(&data[..16]);
- let v6 = IpAddr::V6(Ipv6Addr::from(octets));
- let _ = is_blocked_ip(&v6);
- let v4 = Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3]);
- let blocked_v4 = is_blocked_ip(&IpAddr::V4(v4));
- // `::ffff:a.b.c.d` reaches the same host as `a.b.c.d`, so the guard must
- // return the same verdict for both.
- let mapped = IpAddr::V6(v4.to_ipv6_mapped());
- assert_eq!(
- is_blocked_ip(&mapped),
- blocked_v4,
- "IPv4-mapped address was judged differently from its embedded IPv4"
- );
- });
|