mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-24 13:16:28 +00:00
fix(trusted-proxies): validate full Forwarded chain and bare IPv6 (#4461)
This commit is contained in:
@@ -333,45 +333,52 @@ impl ProxyValidator {
|
||||
}
|
||||
|
||||
/// Parses the RFC 7239 "Forwarded" header value.
|
||||
///
|
||||
/// Every comma-separated element is parsed in wire order (client-closest
|
||||
/// first, each proxy appends its node to the right) so that the resulting
|
||||
/// `proxy_chain` can be validated by the same right-to-left chain analysis
|
||||
/// used for `X-Forwarded-For`. This prevents a client from spoofing the
|
||||
/// real IP by supplying only the leftmost element.
|
||||
///
|
||||
/// `host`/`proto` are taken solely from the last (most recent) element,
|
||||
/// which is appended by the directly connected trusted proxy — mirroring
|
||||
/// how the legacy path reads proxy-set `X-Forwarded-Host`/`Proto` headers.
|
||||
/// Values injected by the client in earlier elements are ignored.
|
||||
fn parse_forwarded_header(header_value: &str, proxy_ip: IpAddr) -> Option<ParsedHeaders> {
|
||||
// Simplified implementation: processes only the first entry in the header.
|
||||
let first_part = header_value.split(',').next()?.trim();
|
||||
let elements: Vec<&str> = header_value
|
||||
.split(',')
|
||||
.map(|element| element.trim())
|
||||
.filter(|element| !element.is_empty())
|
||||
.collect();
|
||||
|
||||
let mut proxy_chain = Vec::new();
|
||||
let mut forwarded_host = None;
|
||||
let mut forwarded_proto = None;
|
||||
|
||||
for part in first_part.split(';') {
|
||||
let part = part.trim();
|
||||
if let Some((key, value)) = part.split_once('=') {
|
||||
let key = key.trim().to_lowercase();
|
||||
let value = value.trim().trim_matches('"');
|
||||
let last_index = elements.len().saturating_sub(1);
|
||||
for (index, element) in elements.iter().enumerate() {
|
||||
let is_last = index == last_index;
|
||||
for part in element.split(';') {
|
||||
let part = part.trim();
|
||||
if let Some((key, value)) = part.split_once('=') {
|
||||
let key = key.trim().to_lowercase();
|
||||
let value = value.trim().trim_matches('"');
|
||||
|
||||
match key.as_str() {
|
||||
"for" => {
|
||||
// Extract IP address, handling IPv6 addresses in brackets as per RFC 7239.
|
||||
let ip_str = if value.starts_with('[') {
|
||||
if let Some(end) = value.find(']') {
|
||||
&value[1..end]
|
||||
} else {
|
||||
continue; // Invalid format, skip
|
||||
match key.as_str() {
|
||||
"for" => {
|
||||
if let Some(ip) = Self::parse_forwarded_node(value) {
|
||||
proxy_chain.push(ip);
|
||||
}
|
||||
} else {
|
||||
// For IPv4 or IPv6 without brackets, take the part before the first colon.
|
||||
value.split(':').next().unwrap_or(value)
|
||||
};
|
||||
|
||||
if let Ok(ip) = ip_str.parse::<IpAddr>() {
|
||||
proxy_chain.push(ip);
|
||||
}
|
||||
// Only honor host/proto from the trusted proxy-appended node.
|
||||
"host" if is_last => {
|
||||
forwarded_host = Some(value.to_string());
|
||||
}
|
||||
"proto" if is_last => {
|
||||
forwarded_proto = Some(value.to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
"host" => {
|
||||
forwarded_host = Some(value.to_string());
|
||||
}
|
||||
"proto" => {
|
||||
forwarded_proto = Some(value.to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -394,23 +401,36 @@ impl ProxyValidator {
|
||||
.split(',')
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.filter_map(|s| {
|
||||
// Handle IPv6 addresses in brackets, e.g., [::1]:8080
|
||||
let ip_str = if s.starts_with('[') {
|
||||
if let Some(end) = s.find(']') {
|
||||
&s[1..end]
|
||||
} else {
|
||||
s // Invalid format, try parsing as is
|
||||
}
|
||||
} else {
|
||||
// For IPv4 or IPv6 without brackets, take the part before the first colon.
|
||||
s.split(':').next().unwrap_or(s)
|
||||
};
|
||||
ip_str.parse::<IpAddr>().ok()
|
||||
})
|
||||
.filter_map(Self::parse_forwarded_node)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parses a single forwarded node token into an `IpAddr`.
|
||||
///
|
||||
/// Handles bracketed IPv6 (`[2001:db8::1]` and `[2001:db8::1]:443`),
|
||||
/// IPv4 with an optional `:port`, and BARE IPv6 addresses. A trailing
|
||||
/// `:port` is only stripped when the token is not itself a valid bare
|
||||
/// address, so `2001:db8::1` is no longer truncated to `2001`.
|
||||
fn parse_forwarded_node(token: &str) -> Option<IpAddr> {
|
||||
let token = token.trim();
|
||||
if token.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let ip_str = if let Some(rest) = token.strip_prefix('[') {
|
||||
// Bracketed IPv6, optionally followed by ":port".
|
||||
rest.split(']').next()?
|
||||
} else if token.parse::<IpAddr>().is_ok() {
|
||||
// Bare address (IPv4 or IPv6) with no port; parse the whole token.
|
||||
token
|
||||
} else {
|
||||
// Not a bare address: strip a trailing ":port" (IPv4 host:port).
|
||||
token.rsplit_once(':').map(|(host, _)| host).unwrap_or(token)
|
||||
};
|
||||
|
||||
ip_str.parse::<IpAddr>().ok()
|
||||
}
|
||||
|
||||
/// Records the start of a validation attempt in metrics.
|
||||
fn record_metric_start(&self) {
|
||||
if let Some(metrics) = &self.metrics {
|
||||
|
||||
Reference in New Issue
Block a user