@@ -7110,6 +7110,195 @@ network_policies:
71107110 assert ! ( result. is_err( ) ) ;
71117111 }
71127112
7113+ // -- parse_target: CONNECT target parser regression tests --
7114+
7115+ #[ test]
7116+ fn test_parse_target_valid_baseline ( ) {
7117+ let ( host, port) = parse_target ( "example.com:443" ) . unwrap ( ) ;
7118+ assert_eq ! ( host, "example.com" ) ;
7119+ assert_eq ! ( port, 443 ) ;
7120+ }
7121+
7122+ #[ test]
7123+ fn test_parse_target_preserves_case ( ) {
7124+ let ( host, port) = parse_target ( "EXAMPLE.COM:443" ) . unwrap ( ) ;
7125+ assert_eq ! ( host, "EXAMPLE.COM" , "parse_target should preserve case" ) ;
7126+ assert_eq ! ( port, 443 ) ;
7127+ }
7128+
7129+ #[ test]
7130+ fn test_parse_target_accepts_empty_host ( ) {
7131+ let ( host, port) = parse_target ( ":443" ) . unwrap ( ) ;
7132+ assert ! ( host. is_empty( ) , "empty host accepted without validation" ) ;
7133+ assert_eq ! ( port, 443 ) ;
7134+ }
7135+
7136+ #[ test]
7137+ fn test_parse_target_nul_byte_passes_through ( ) {
7138+ let ( host, _) = parse_target ( "evil.com\0 .safe.com:443" ) . unwrap ( ) ;
7139+ assert_eq ! (
7140+ host, "evil.com\0 .safe.com" ,
7141+ "NUL byte not stripped or rejected"
7142+ ) ;
7143+ }
7144+
7145+ #[ test]
7146+ fn test_parse_target_control_char_passes_through ( ) {
7147+ let ( host, _) = parse_target ( "evil\x01 .com:443" ) . unwrap ( ) ;
7148+ assert ! (
7149+ host. contains( '\x01' ) ,
7150+ "control characters pass through without validation"
7151+ ) ;
7152+ }
7153+
7154+ #[ test]
7155+ fn test_parse_target_percent_encoded_dot_is_literal ( ) {
7156+ let ( host, _) = parse_target ( "evil%2ecom:443" ) . unwrap ( ) ;
7157+ assert_eq ! (
7158+ host, "evil%2ecom" ,
7159+ "percent-encoded dot not decoded — literal %2e in host"
7160+ ) ;
7161+ }
7162+
7163+ #[ test]
7164+ fn test_parse_target_percent_encoded_nul_is_literal ( ) {
7165+ let ( host, _) = parse_target ( "evil%00.safe.com:443" ) . unwrap ( ) ;
7166+ assert_eq ! (
7167+ host, "evil%00.safe.com" ,
7168+ "percent-encoded NUL not decoded — literal %00 in host"
7169+ ) ;
7170+ }
7171+
7172+ #[ test]
7173+ fn test_parse_target_rejects_missing_port_separator ( ) {
7174+ assert ! (
7175+ parse_target( "hostonly" ) . is_err( ) ,
7176+ "missing colon should be rejected"
7177+ ) ;
7178+ }
7179+
7180+ #[ test]
7181+ fn test_parse_target_rejects_non_numeric_port ( ) {
7182+ assert ! (
7183+ parse_target( "host:notaport" ) . is_err( ) ,
7184+ "non-numeric port should be rejected"
7185+ ) ;
7186+ }
7187+
7188+ #[ test]
7189+ fn test_parse_target_rejects_port_overflow ( ) {
7190+ assert ! (
7191+ parse_target( "host:65536" ) . is_err( ) ,
7192+ "port > 65535 should be rejected by u16 parse"
7193+ ) ;
7194+ }
7195+
7196+ #[ test]
7197+ fn test_parse_target_accepts_port_zero ( ) {
7198+ let ( _, port) = parse_target ( "host:0" ) . unwrap ( ) ;
7199+ assert_eq ! ( port, 0 ) ;
7200+ }
7201+
7202+ #[ test]
7203+ fn test_parse_target_accepts_port_max ( ) {
7204+ let ( _, port) = parse_target ( "host:65535" ) . unwrap ( ) ;
7205+ assert_eq ! ( port, 65535 ) ;
7206+ }
7207+
7208+ #[ test]
7209+ fn test_parse_target_bracket_chars_pass_through ( ) {
7210+ let ( host, _) = parse_target ( "a]b[c:443" ) . unwrap ( ) ;
7211+ assert_eq ! ( host, "a]b[c" , "brackets pass through without validation" ) ;
7212+ }
7213+
7214+ #[ test]
7215+ fn test_parse_target_oversized_hostname_accepted ( ) {
7216+ let long_host = "a" . repeat ( 254 ) ;
7217+ let target = format ! ( "{long_host}:443" ) ;
7218+ let ( host, _) = parse_target ( & target) . unwrap ( ) ;
7219+ assert_eq ! (
7220+ host. len( ) ,
7221+ 254 ,
7222+ "hostname exceeding DNS 253-char limit not rejected"
7223+ ) ;
7224+ }
7225+
7226+ #[ test]
7227+ fn test_parse_target_backslash_passes_through ( ) {
7228+ let ( host, _) = parse_target ( "evil.com\\ ..safe.com:443" ) . unwrap ( ) ;
7229+ assert ! (
7230+ host. contains( '\\' ) ,
7231+ "backslash passes through without validation"
7232+ ) ;
7233+ }
7234+
7235+ #[ test]
7236+ fn test_parse_target_slash_passes_through ( ) {
7237+ let ( host, _) = parse_target ( "evil.com/../safe.com:443" ) . unwrap ( ) ;
7238+ assert ! (
7239+ host. contains( '/' ) ,
7240+ "forward slash passes through without validation"
7241+ ) ;
7242+ }
7243+
7244+ #[ test]
7245+ fn test_parse_target_extra_colon_fails_port_parse ( ) {
7246+ assert ! (
7247+ parse_target( "host:80:extra" ) . is_err( ) ,
7248+ "trailing content after port should fail u16 parse"
7249+ ) ;
7250+ }
7251+
7252+ #[ test]
7253+ fn test_parse_target_ipv6_bracket_notation_fails ( ) {
7254+ assert ! (
7255+ parse_target( "[::1]:443" ) . is_err( ) ,
7256+ "split_once splits at first colon inside brackets — port parse fails"
7257+ ) ;
7258+ }
7259+
7260+ // -- parse_proxy_uri: hostname parser regression tests --
7261+
7262+ #[ test]
7263+ fn test_parse_proxy_uri_nul_byte_in_host ( ) {
7264+ let ( _, host, port, _) = parse_proxy_uri ( "http://evil.com\0 .safe.com:80/path" ) . unwrap ( ) ;
7265+ assert_eq ! (
7266+ host, "evil.com\0 .safe.com" ,
7267+ "NUL byte not stripped or rejected in forward proxy URI"
7268+ ) ;
7269+ assert_eq ! ( port, 80 ) ;
7270+ }
7271+
7272+ #[ test]
7273+ fn test_parse_proxy_uri_control_char_in_host ( ) {
7274+ let ( _, host, _, _) = parse_proxy_uri ( "http://evil\x01 .com:80/" ) . unwrap ( ) ;
7275+ assert ! (
7276+ host. contains( '\x01' ) ,
7277+ "control characters pass through without validation"
7278+ ) ;
7279+ }
7280+
7281+ #[ test]
7282+ fn test_parse_proxy_uri_percent_encoded_dot_in_host ( ) {
7283+ let ( _, host, _, _) = parse_proxy_uri ( "http://evil%2ecom:80/" ) . unwrap ( ) ;
7284+ assert_eq ! (
7285+ host, "evil%2ecom" ,
7286+ "percent-encoded dot not decoded — literal %2e in host"
7287+ ) ;
7288+ }
7289+
7290+ #[ test]
7291+ fn test_parse_proxy_uri_oversized_hostname ( ) {
7292+ let long_host = "a" . repeat ( 254 ) ;
7293+ let uri = format ! ( "http://{long_host}:80/" ) ;
7294+ let ( _, host, _, _) = parse_proxy_uri ( & uri) . unwrap ( ) ;
7295+ assert_eq ! (
7296+ host. len( ) ,
7297+ 254 ,
7298+ "hostname exceeding DNS 253-char limit not rejected"
7299+ ) ;
7300+ }
7301+
71137302 // --- rewrite_forward_request tests ---
71147303
71157304 #[ tokio:: test]
@@ -7612,6 +7801,28 @@ network_policies:
76127801 ) ;
76137802 }
76147803
7804+ // -- SSRF: malformed hostname resolution regression tests --
7805+
7806+ #[ tokio:: test]
7807+ async fn test_resolve_reject_internal_fails_closed_on_nul_hostname ( ) {
7808+ let result = resolve_and_reject_internal ( "evil.com\0 .safe.com" , 443 , 0 ) . await ;
7809+ assert ! (
7810+ result. is_err( ) ,
7811+ "NUL-containing hostname should fail DNS resolution (fail closed)"
7812+ ) ;
7813+ }
7814+
7815+ #[ tokio:: test]
7816+ async fn test_resolve_allowed_ips_fails_closed_on_nul_hostname ( ) {
7817+ let nets = parse_allowed_ips ( & [ "0.0.0.0/0" . to_string ( ) ] )
7818+ . unwrap_or_else ( |_| vec ! [ "0.0.0.0/0" . parse:: <ipnet:: IpNet >( ) . unwrap( ) ] ) ;
7819+ let result = resolve_and_check_allowed_ips ( "evil.com\0 .safe.com" , 443 , & nets, 0 ) . await ;
7820+ assert ! (
7821+ result. is_err( ) ,
7822+ "NUL-containing hostname should fail DNS resolution (fail closed)"
7823+ ) ;
7824+ }
7825+
76157826 // -- implicit_allowed_ips_for_ip_host --
76167827
76177828 #[ test]
0 commit comments