Close outbound credential and metadata bypasses
This commit is contained in:
parent
3088aa439e
commit
c4402985af
2 changed files with 194 additions and 23 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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