ssrf_url.rs 1.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
  1. //! Fuzz the SSRF URL gate and address classifier (`protocol::ssrf`).
  2. //!
  3. //! [`validate_fetch_url`] is pure: an accepted URL is always `https`, has a
  4. //! non-empty host, and carries no credentials. The classifier half maps the
  5. //! first 16 input bytes to an [`IpAddr`] and calls [`is_blocked_ip`]; an
  6. //! IPv4-mapped address must be judged exactly like its embedded IPv4 address.
  7. //! [`fetch_guarded_bytes_with`] is deliberately not called: it is async, opens
  8. //! a socket, and is non-hermetic.
  9. #![no_main]
  10. use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
  11. use d_oauth::protocol::ssrf::{is_blocked_ip, validate_fetch_url};
  12. use libfuzzer_sys::fuzz_target;
  13. fuzz_target!(|data: &[u8]| {
  14. if let Ok(raw) = core::str::from_utf8(data)
  15. && let Ok(url) = validate_fetch_url(raw)
  16. {
  17. assert_eq!(url.scheme(), "https", "accepted URL is not https");
  18. assert!(
  19. url.host_str().is_some_and(|host| !host.is_empty()),
  20. "accepted URL has no host"
  21. );
  22. assert!(url.username().is_empty(), "accepted URL kept a username");
  23. assert!(url.password().is_none(), "accepted URL kept a password");
  24. }
  25. if data.len() < 16 {
  26. return;
  27. }
  28. let mut octets = [0u8; 16];
  29. octets.copy_from_slice(&data[..16]);
  30. let v6 = IpAddr::V6(Ipv6Addr::from(octets));
  31. let _ = is_blocked_ip(&v6);
  32. let v4 = Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3]);
  33. let blocked_v4 = is_blocked_ip(&IpAddr::V4(v4));
  34. // `::ffff:a.b.c.d` reaches the same host as `a.b.c.d`, so the guard must
  35. // return the same verdict for both.
  36. let mapped = IpAddr::V6(v4.to_ipv6_mapped());
  37. assert_eq!(
  38. is_blocked_ip(&mapped),
  39. blocked_v4,
  40. "IPv4-mapped address was judged differently from its embedded IPv4"
  41. );
  42. });