Close outbound credential and metadata bypasses

This commit is contained in:
ms2k 2026-07-17 03:41:43 +09:00
commit c4402985af

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"
)
}