Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/plugins/http/csrf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
14 changes: 10 additions & 4 deletions src/plugins/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())?;
}

Expand Down Expand Up @@ -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(());
Expand Down Expand Up @@ -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()
};
Expand Down
5 changes: 4 additions & 1 deletion src/plugins/port_scanner/grabbers/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(';') {
Expand Down
33 changes: 28 additions & 5 deletions src/utils/target/multi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,21 @@ fn parse_multiple_targets_atom(expression: &str) -> Result<Vec<String>, 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<u8, Error> {
m.as_str().parse::<u8>().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!(
Expand Down Expand Up @@ -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![
Expand Down
Loading