Close outbound credential and metadata bypasses

This commit is contained in:
ms2k 2026-07-17 03:41:43 +09:00
commit c4402985af
2 changed files with 194 additions and 23 deletions

View file

@ -7,9 +7,6 @@ use ipnet::IpNet;
use crate::{CanonicalUrl, OutboundError, Resolver};
const METADATA_V4: &str = "169.254.169.254";
const METADATA_V6: &str = "fd00:ec2::254";
#[derive(Clone, Debug)]
pub struct OutboundPolicy {
allowed_scheme_ports: HashSet<(String, u16)>,
@ -75,7 +72,7 @@ impl OutboundPolicy {
IpAddr::V6(address) => address.to_ipv4_mapped().map_or(address.into(), IpAddr::V4),
address => address,
};
if effective.to_string() == METADATA_V4 || effective.to_string() == METADATA_V6 {
if is_metadata_capable_space(effective) {
return false;
}
if self
@ -158,6 +155,18 @@ fn is_global_unicast(address: IpAddr) -> bool {
}
}
fn is_metadata_capable_space(address: IpAddr) -> bool {
match address {
IpAddr::V4(address) => {
let [first, second, third, _] = address.octets();
(first == 100 && (64..=127).contains(&second))
|| (first == 169 && second == 254)
|| (first == 192 && second == 0 && third == 0)
}
IpAddr::V6(address) => address.segments()[0] & 0xfe00 == 0xfc00,
}
}
fn is_global_ipv4(address: Ipv4Addr) -> bool {
if address.is_unspecified()
|| address.is_loopback()

View file

@ -1,4 +1,4 @@
use std::net::SocketAddr;
use std::{fmt, net::SocketAddr};
use http::{HeaderMap, HeaderName, Method, StatusCode};
use reqwest::{Certificate, redirect::Policy};
@ -25,11 +25,11 @@ pub struct OutboundClientBuilder<R> {
client: OutboundClient<R>,
}
#[derive(Clone, Debug)]
pub struct OutboundRequest {
method: Method,
url: CanonicalUrl,
headers: HeaderMap,
credential_headers: HeaderMap,
}
#[derive(Debug)]
@ -58,9 +58,11 @@ impl<R: Resolver> OutboundClient<R> {
loop {
let endpoint = self.policy.authorize(&request.url, &self.resolver).await?;
let client = self.client_for(&request.url, &endpoint)?;
let mut headers = request.headers.clone();
headers.extend(request.credential_headers.clone());
let response = client
.request(request.method.clone(), request.url.as_url().clone())
.headers(request.headers.clone())
.headers(headers)
.send()
.await
.map_err(|_| OutboundError::Transport)?;
@ -84,7 +86,7 @@ impl<R: Resolver> OutboundClient<R> {
return Err(OutboundError::HttpsDowngrade);
}
if request.url.origin()? != next.origin()? {
strip_cross_origin_credentials(&mut request.headers);
request.credential_headers.clear();
}
request.url = next;
redirects += 1;
@ -131,6 +133,7 @@ impl OutboundRequest {
method: Method::GET,
url: CanonicalUrl::parse(url)?,
headers: HeaderMap::new(),
credential_headers: HeaderMap::new(),
})
}
@ -139,19 +142,42 @@ impl OutboundRequest {
method: Method::HEAD,
url: CanonicalUrl::parse(url)?,
headers: HeaderMap::new(),
credential_headers: HeaderMap::new(),
})
}
pub fn with_headers(mut self, headers: HeaderMap) -> Result<Self, OutboundError> {
if headers.contains_key(http::header::HOST)
|| headers.contains_key(http::header::CONNECTION)
|| headers.contains_key(http::header::PROXY_AUTHORIZATION)
pub fn with_public_headers(mut self, headers: HeaderMap) -> Result<Self, OutboundError> {
if headers
.keys()
.any(|name| !is_public_header(name) || self.credential_headers.contains_key(name))
{
return Err(OutboundError::InvalidHeaders);
}
self.headers = headers;
Ok(self)
}
pub fn with_credential_headers(mut self, headers: HeaderMap) -> Result<Self, OutboundError> {
if headers
.keys()
.any(|name| is_connection_controlled_header(name) || self.headers.contains_key(name))
{
return Err(OutboundError::InvalidHeaders);
}
self.credential_headers = headers;
Ok(self)
}
}
impl fmt::Debug for OutboundRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("OutboundRequest")
.field("method", &self.method)
.field("header_count", &self.headers.len())
.field("credential_header_count", &self.credential_headers.len())
.finish_non_exhaustive()
}
}
impl OutboundResponse {
@ -171,12 +197,37 @@ impl OutboundResponse {
}
}
fn strip_cross_origin_credentials(headers: &mut HeaderMap) {
for name in [
http::header::AUTHORIZATION,
http::header::COOKIE,
HeaderName::from_static("cookie2"),
] {
headers.remove(name);
}
fn is_public_header(name: &HeaderName) -> bool {
matches!(
name.as_str(),
"accept"
| "accept-charset"
| "accept-encoding"
| "accept-language"
| "cache-control"
| "if-match"
| "if-modified-since"
| "if-none-match"
| "if-range"
| "if-unmodified-since"
| "pragma"
| "range"
| "user-agent"
)
}
fn is_connection_controlled_header(name: &HeaderName) -> bool {
matches!(
name.as_str(),
"connection"
| "content-length"
| "host"
| "keep-alive"
| "proxy-authenticate"
| "proxy-authorization"
| "te"
| "trailer"
| "transfer-encoding"
| "upgrade"
)
}