From beb1075333c9334fbe4655b313ff2f3bc517d8e5 Mon Sep 17 00:00:00 2001 From: Luigi Colluto Date: Sun, 5 Jul 2026 18:37:29 +0200 Subject: [PATCH 1/2] fix: avoid panic on non-ASCII HTTP response headers Several HTTP paths called HeaderValue::to_str().unwrap() on response headers: the response-context builder, the Location redirect handler, the Set-Cookie reader, the CSRF cookie reader, and the port scanner's HTTP banner grabber. to_str() rejects non-ASCII bytes, but RFC 7230 allows obs-text (0x80..=0xFF) in header values, so a single such byte from a malicious or misbehaving target server panics the process. Since the release profile sets panic = "abort", that panic aborts the whole scan, so a host you merely scan can take down the operator's legba. Decode the header bytes lossily instead of unwrapping. --- src/plugins/http/csrf.rs | 4 +++- src/plugins/http/mod.rs | 14 ++++++++++---- src/plugins/port_scanner/grabbers/http.rs | 5 ++++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/plugins/http/csrf.rs b/src/plugins/http/csrf.rs index f487971..2d680ac 100644 --- a/src/plugins/http/csrf.rs +++ b/src/plugins/http/csrf.rs @@ -64,7 +64,9 @@ pub(crate) async fn handle( if res.status().is_success() { // get cookie from header if let Some(cookie) = res.headers().get("set-cookie") { - token.cookie = cookie.to_str().unwrap().to_owned(); + // decode lossily: a malicious server can send non-ASCII header bytes that + // HeaderValue::to_str would reject, panicking (abort) the process. + token.cookie = String::from_utf8_lossy(cookie.as_bytes()).into_owned(); } else { log::warn!("csrf page unexpectetly did not return any cookie"); } diff --git a/src/plugins/http/mod.rs b/src/plugins/http/mod.rs index 8a850ae..0199101 100644 --- a/src/plugins/http/mod.rs +++ b/src/plugins/http/mod.rs @@ -278,13 +278,18 @@ impl HTTP { continue; } + // header values may contain non-ASCII (obs-text, 0x80..=0xFF) bytes, which + // HeaderValue::to_str rejects; decode them lossily instead of unwrapping so a + // malicious or misbehaving server cannot abort the process (panic = "abort"). + let header_value = String::from_utf8_lossy(value.as_bytes()); + if header_var_name == "content_type" { content_type_set = true; - content_type = value.to_str().unwrap().to_owned(); + content_type = header_value.to_string(); } context - .set_value(header_var_name, Value::from(value.to_str().unwrap())) + .set_value(header_var_name, Value::from(header_value.as_ref())) .map_err(|e| e.to_string())?; } @@ -575,7 +580,8 @@ impl HTTP { if status.is_redirection() && let Some(relocation) = relocation { - let mut relocation = relocation.to_str().unwrap().to_owned(); + let mut relocation = + String::from_utf8_lossy(relocation.as_bytes()).into_owned(); // redirect to a page if relocation.starts_with("/") || relocation.contains(&creds.target) { return Ok(()); @@ -717,7 +723,7 @@ impl HTTP { Err(e) => Err(fmt_request_error(e)), Ok(res) => { let cookie = if let Some(cookie) = res.headers().get(COOKIE) { - cookie.to_str().unwrap().to_owned() + String::from_utf8_lossy(cookie.as_bytes()).into_owned() } else { "".to_owned() }; diff --git a/src/plugins/port_scanner/grabbers/http.rs b/src/plugins/port_scanner/grabbers/http.rs index 118bbd7..5f7902d 100644 --- a/src/plugins/port_scanner/grabbers/http.rs +++ b/src/plugins/port_scanner/grabbers/http.rs @@ -64,7 +64,10 @@ pub(crate) async fn parse_http_response( // collect headers for (name, value) in response.headers() { let name = name.to_string(); - let mut value = value.to_str().unwrap(); + // decode lossily instead of unwrapping: a scanned host can return non-ASCII header + // bytes that HeaderValue::to_str rejects, which would panic (abort) the process. + let value = String::from_utf8_lossy(value.as_bytes()); + let mut value = value.as_ref(); if name == "content-type" { if value.contains(';') { From 1605e74e1fe3f7495d026b4df328939d3477c820 Mon Sep 17 00:00:00 2001 From: Luigi Colluto Date: Sun, 5 Jul 2026 18:37:29 +0200 Subject: [PATCH 2/2] fix: avoid panic on out-of-range IPv4 range octets parse_multiple_targets_atom captured each octet of an IPv4 range target as \d+ and then did parse::().unwrap(), so a target such as "256.0.0.0-1" (any octet > 255) panicked. Under the release profile's panic = "abort" this aborts the process, and the target parser is reachable from the unauthenticated REST/MCP API, so one request can take down the server. Parse the octets fallibly and return an error for out-of-range values. Adds a regression test. --- src/utils/target/multi.rs | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/utils/target/multi.rs b/src/utils/target/multi.rs index c27f031..8574509 100644 --- a/src/utils/target/multi.rs +++ b/src/utils/target/multi.rs @@ -25,11 +25,21 @@ fn parse_multiple_targets_atom(expression: &str) -> Result, Error> { .collect()) } else if let Some(caps) = IPV4_RANGE_PARSER.captures(expression) { // ipv4 range like 192.168.1.1-10 or 192.168.1.1-10:port - let a: u8 = caps.get(1).unwrap().as_str().parse().unwrap(); - let b: u8 = caps.get(2).unwrap().as_str().parse().unwrap(); - let c: u8 = caps.get(3).unwrap().as_str().parse().unwrap(); - let start: u8 = caps.get(4).unwrap().as_str().parse().unwrap(); - let stop: u8 = caps.get(5).unwrap().as_str().parse().unwrap(); + // + // each octet is captured as `\d+`, which also matches out-of-range values such as + // "256.0.0.0-1"; parse fallibly so a bad octet becomes a returned error instead of a + // panic. Under the release profile's `panic = "abort"` such a panic aborts the whole + // process, and this parser is reachable from the unauthenticated REST/MCP API. + let octet = |m: regex::Match| -> Result { + m.as_str().parse::().map_err(|_| { + format!("invalid IPv4 octet '{}' in target '{}'", m.as_str(), expression) + }) + }; + let a = octet(caps.get(1).unwrap())?; + let b = octet(caps.get(2).unwrap())?; + let c = octet(caps.get(3).unwrap())?; + let start = octet(caps.get(4).unwrap())?; + let stop = octet(caps.get(5).unwrap())?; if stop < start { return Err(format!( @@ -142,6 +152,19 @@ mod tests { assert!(res.is_err()); } + #[test] + fn ipv4_range_out_of_range_octet_errors_without_panic() { + // regression: octets are captured as `\d+`, so values > 255 must return an error + // rather than panic (which aborts under the release `panic = "abort"` profile). + for target in ["256.0.0.0-1", "1.2.3.4-999", "1.2.3.300-1", "999.0.0.0-0"] { + assert!( + parse_multiple_targets(target).is_err(), + "expected an error for out-of-range range target {:?}", + target + ); + } + } + #[test] fn can_parse_comma_separated() { let expected = Ok(vec![