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(';') { 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![