Files
codex/codex-rs/network-proxy/src/authorization_path.rs
kevinlin-openai 7a0e974e08 Harden network proxy MITM authorization (#37211)
## Why

MITM hooks authorize requests before the upstream server parses them. Paths that
can be decoded or normalized to a different resource must not match an allowed
path, and hosts that require MITM inspection must not bypass it through the
plain HTTP proxy path.

## What changed

- Reject ambiguous hook paths, including traversal segments, backslashes,
  malformed percent encodings, and encoded separators or percent signs.
- Block plain HTTP proxy requests for hosts whose policy always requires MITM,
  recording the decision as `mitm_required`.

## Testing

- Cover safe and ambiguous path forms, encoded traversal through repository
  allowlists, and absolute-form HTTPS requests sent to the HTTP proxy.

GitOrigin-RevId: 8812a980ac64a97cbac3a237376d29be5ded9220
2026-08-06 03:57:10 +00:00

65 lines
1.9 KiB
Rust

/// Returns whether `path` has an unambiguous interpretation for authorization.
///
/// MITM hooks authorize the request before the upstream server parses it. Reject
/// path forms that common upstreams may decode or normalize into a different
/// resource after a hook has matched.
pub(crate) fn is_safe_for_authorization(path: &str) -> bool {
path.split('/').all(is_safe_segment_for_authorization)
}
fn is_safe_segment_for_authorization(segment: &str) -> bool {
let bytes = segment.as_bytes();
let mut index = 0;
let mut decoded_dots = 0;
let mut has_non_dot = false;
while index < bytes.len() {
match bytes[index] {
b'.' => {
decoded_dots += 1;
index += 1;
}
b'\\' => return false,
b'%' => {
let Some(high) = bytes
.get(index + 1)
.and_then(|byte| decode_hex_digit(*byte))
else {
return false;
};
let Some(low) = bytes
.get(index + 2)
.and_then(|byte| decode_hex_digit(*byte))
else {
return false;
};
let decoded = high << 4 | low;
match decoded {
b'%' | b'/' | b'\\' => return false,
b'.' => decoded_dots += 1,
_ => has_non_dot = true,
}
index += 3;
}
_ => {
has_non_dot = true;
index += 1;
}
}
}
has_non_dot || !matches!(decoded_dots, 1 | 2)
}
fn decode_hex_digit(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
#[cfg(test)]
#[path = "authorization_path_tests.rs"]
mod tests;