Close outbound credential and metadata bypasses
This commit is contained in:
parent
3088aa439e
commit
c4402985af
1 changed files with 194 additions and 23 deletions
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue