Compare commits

...

4 Commits

Author SHA1 Message Date
houseme
4ac9143165 fix 2025-12-27 01:22:03 +08:00
houseme
a5695f35d2 add ipnet crates 2025-12-27 01:08:52 +08:00
houseme
cd9b5ad3f3 fix 2025-12-25 11:35:40 +08:00
houseme
04833f35cf Security fix: Add trusted proxy validation for SourceIP bypass vulnerability 2025-12-25 01:11:05 +08:00
5 changed files with 212 additions and 24 deletions

1
Cargo.lock generated
View File

@@ -7658,6 +7658,7 @@ dependencies = [
"hmac 0.13.0-rc.3",
"http 1.4.0",
"hyper 1.8.1",
"ipnet",
"libc",
"local-ip-address",
"lz4",

View File

@@ -122,6 +122,7 @@ tonic-prost = { version = "0.14.2" }
tonic-prost-build = { version = "0.14.2" }
tower = { version = "0.5.2", features = ["timeout"] }
tower-http = { version = "0.6.8", features = ["cors"] }
ipnet = "2.11.0"
# Serialization and Data Formats
bytes = { version = "1.11.0", features = ["serde"] }

View File

@@ -65,6 +65,7 @@ tracing = { workspace = true }
transform-stream = { workspace = true, optional = true }
url = { workspace = true, optional = true }
zstd = { workspace = true, optional = true }
ipnet = { workspace = true, optional = true }
[dev-dependencies]
tempfile = { workspace = true }
@@ -92,5 +93,5 @@ hash = ["dep:highway", "dep:md-5", "dep:sha2", "dep:blake3", "dep:serde", "dep:s
os = ["dep:nix", "dep:tempfile", "winapi"] # operating system utilities
integration = [] # integration test features
sys = ["dep:sysinfo"] # system information features
http = ["dep:convert_case", "dep:http", "dep:regex"]
http = ["dep:convert_case", "dep:http", "dep:regex", "dep:ipnet"] # http utilities
full = ["ip", "tls", "net", "io", "hash", "os", "integration", "path", "crypto", "string", "compress", "sys", "notify", "http"] # all features

View File

@@ -13,9 +13,10 @@
// limitations under the License.
use http::HeaderMap;
use ipnet::{IpNet, Ipv4Net, Ipv6Net};
use regex::Regex;
use std::env;
use std::net::SocketAddr;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::str::FromStr;
use std::sync::LazyLock;
@@ -45,6 +46,100 @@ fn is_xff_header_enabled() -> bool {
== "on"
}
/// TrustedProxies holds configuration for validating proxy sources
#[derive(Debug, Clone)]
pub struct TrustedProxies {
/// List of trusted proxy IP networks (CIDR format)
pub cidrs: Vec<IpNet>,
/// Whether to enable proxy validation
pub enable_validation: bool,
/// Maximum allowed proxy chain length
pub max_chain_length: usize,
}
impl TrustedProxies {
/// Create a new TrustedProxies configuration
pub fn new(cidrs: Vec<String>, enable_validation: bool, max_chain_length: usize) -> Self {
let cidrs = cidrs.into_iter().filter_map(|s| s.parse::<IpNet>().ok()).collect();
Self {
cidrs,
enable_validation,
max_chain_length,
}
}
/// Check if an IP address is within the trusted proxy ranges
pub fn is_trusted_proxy(&self, ip: IpAddr) -> bool {
if !self.enable_validation {
return true; // Backward compatibility: trust all when disabled
}
self.cidrs.iter().any(|net| net.contains(&ip))
}
}
impl Default for TrustedProxies {
fn default() -> Self {
Self {
cidrs: vec![],
enable_validation: true,
max_chain_length: 10,
}
}
}
/// Validate if an IP string represents a valid client IP
/// Returns false for private/loopback addresses and invalid formats
fn is_valid_client_ip(ip_str: &str, max_chain_length: usize) -> bool {
// Handle X-Forwarded-For chains
if ip_str.contains(',') {
let parts: Vec<&str> = ip_str.split(',').map(|s| s.trim()).collect();
if parts.len() > max_chain_length {
return false;
}
// Validate each IP in the chain
for part in parts {
if !is_valid_single_ip(part) {
return false;
}
}
return true;
}
is_valid_single_ip(ip_str)
}
/// Validate a single IP address string
fn is_valid_single_ip(ip_str: &str) -> bool {
match ip_str.parse::<IpAddr>() {
Ok(ip) => {
// Reject private and loopback addresses as client IPs
// (they should come from trusted proxies only)
!is_private(ip) && !ip.is_loopback()
}
Err(_) => false,
}
}
/// Check if an IP address is private
///
/// # Arguments
/// * `ip` - The IP address to check
///
/// # Returns
/// A `bool` indicating whether the IP is private
///
fn is_private(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(ipv4) => ipv4.is_private(),
IpAddr::V6(ipv6) => {
// Check if it's in fc00::/7 (Unique Local Address)
let octets = ipv6.octets();
(octets[0] & 0xfe) == 0xfc
}
}
}
/// GetSourceScheme retrieves the scheme from the X-Forwarded-Proto and RFC7239
/// Forwarded headers (in that order).
///
@@ -147,18 +242,43 @@ pub fn get_source_ip_from_headers(headers: &HeaderMap) -> Option<String> {
addr
}
/// GetSourceIPRaw retrieves the IP from the request headers
/// and falls back to remote_addr when necessary.
/// however returns without bracketing.
/// GetSourceIPRaw retrieves the IP from the request headers with trusted proxy validation
/// and falls back to peer_addr when necessary.
///
/// # Arguments
/// * `headers` - HTTP headers from the request
/// * `remote_addr` - Remote address as a string
/// * `peer_addr` - Peer IP address from the connection
/// * `trusted_proxies` - Trusted proxy configuration
///
/// # Returns
/// A `String` containing the source IP address
/// A `String` containing the validated source IP address
///
pub fn get_source_ip_raw(headers: &HeaderMap, remote_addr: &str) -> String {
pub fn get_source_ip_raw(headers: &HeaderMap, peer_addr: IpAddr, trusted_proxies: &TrustedProxies) -> String {
// If validation is disabled, use legacy behavior for backward compatibility
if !trusted_proxies.enable_validation {
let remote_addr_str = peer_addr.to_string();
return get_source_ip_raw_legacy(headers, &remote_addr_str);
}
// Check if the direct connection is from a trusted proxy
if trusted_proxies.is_trusted_proxy(peer_addr) {
// Trusted proxy: try to get real client IP from headers
if let Some(header_ip) = get_source_ip_from_headers(headers) {
// Validate the IP from headers
if is_valid_client_ip(&header_ip, trusted_proxies.max_chain_length) {
return header_ip;
}
// If header IP is invalid, log warning and fall back to peer
tracing::warn!("Invalid client IP in headers from trusted proxy {}: {}", peer_addr, header_ip);
}
}
// Untrusted source or no valid header IP: use connection peer address
peer_addr.to_string()
}
/// Legacy GetSourceIPRaw for backward compatibility when validation is disabled
fn get_source_ip_raw_legacy(headers: &HeaderMap, remote_addr: &str) -> String {
let addr = get_source_ip_from_headers(headers).unwrap_or_else(|| remote_addr.to_string());
// Default to remote address if headers not set.
@@ -169,19 +289,20 @@ pub fn get_source_ip_raw(headers: &HeaderMap, remote_addr: &str) -> String {
}
}
/// GetSourceIP retrieves the IP from the request headers
/// and falls back to remote_addr when necessary.
/// GetSourceIP retrieves the IP from the request headers with trusted proxy validation
/// and falls back to peer_addr when necessary.
/// It brackets IPv6 addresses.
///
/// # Arguments
/// * `headers` - HTTP headers from the request
/// * `remote_addr` - Remote address as a string
/// * `peer_addr` - Peer IP address from the connection
/// * `trusted_proxies` - Trusted proxy configuration
///
/// # Returns
/// A `String` containing the source IP address, with IPv6 addresses bracketed
///
pub fn get_source_ip(headers: &HeaderMap, remote_addr: &str) -> String {
let addr = get_source_ip_raw(headers, remote_addr);
pub fn get_source_ip(headers: &HeaderMap, peer_addr: IpAddr, trusted_proxies: &TrustedProxies) -> String {
let addr = get_source_ip_raw(headers, peer_addr, trusted_proxies);
if addr.contains(':') { format!("[{addr}]") } else { addr }
}
@@ -210,18 +331,58 @@ mod tests {
}
#[test]
fn test_get_source_ip_raw() {
fn test_trusted_proxies_validation() {
let trusted_proxies = TrustedProxies::new(vec!["192.168.1.0/24".to_string(), "10.0.0.0/8".to_string()], true, 5);
// Trusted IPs
assert!(trusted_proxies.is_trusted_proxy("192.168.1.1".parse().unwrap()));
assert!(trusted_proxies.is_trusted_proxy("10.1.1.1".parse().unwrap()));
// Untrusted IPs
assert!(!trusted_proxies.is_trusted_proxy("203.0.113.1".parse().unwrap()));
}
#[test]
fn test_get_source_ip_raw_with_trusted_proxy() {
let mut headers = HeaderMap::new();
headers.insert("x-forwarded-for", HeaderValue::from_static("203.0.113.1"));
let trusted_proxies = TrustedProxies::new(vec!["192.168.1.1/32".to_string()], true, 5);
let peer_addr: IpAddr = "192.168.1.1".parse().unwrap();
let result = get_source_ip_raw(&headers, peer_addr, &trusted_proxies);
assert_eq!(result, "203.0.113.1");
}
#[test]
fn test_get_source_ip_raw_with_untrusted_proxy() {
let mut headers = HeaderMap::new();
headers.insert("x-forwarded-for", HeaderValue::from_static("203.0.113.1"));
let trusted_proxies = TrustedProxies::new(vec![], true, 5);
let peer_addr: IpAddr = "203.0.113.2".parse().unwrap();
let result = get_source_ip_raw(&headers, peer_addr, &trusted_proxies);
assert_eq!(result, "203.0.113.2"); // Should use peer_addr
}
#[test]
fn test_get_source_ip_raw_legacy_mode() {
let headers = create_test_headers();
let remote_addr = "127.0.0.1:8080";
let result = get_source_ip_raw(&headers, remote_addr);
assert_eq!(result, "192.168.1.1");
let trusted_proxies = TrustedProxies::new(vec![], false, 5); // Disabled validation
let peer_addr: IpAddr = "127.0.0.1".parse().unwrap();
let result = get_source_ip_raw(&headers, peer_addr, &trusted_proxies);
assert_eq!(result, "192.168.1.1"); // Should use header IP
}
#[test]
fn test_get_source_ip() {
let headers = create_test_headers();
let remote_addr = "127.0.0.1:8080";
let result = get_source_ip(&headers, remote_addr);
let trusted_proxies = TrustedProxies::new(vec!["192.168.1.1/32".to_string()], true, 5);
let peer_addr: IpAddr = "192.168.1.1".parse().unwrap();
let result = get_source_ip(&headers, peer_addr, &trusted_proxies);
assert_eq!(result, "192.168.1.1");
}
@@ -229,8 +390,32 @@ mod tests {
fn test_get_source_ip_ipv6() {
let mut headers = HeaderMap::new();
headers.insert("x-forwarded-for", HeaderValue::from_static("2001:db8::1"));
let remote_addr = "127.0.0.1:8080";
let result = get_source_ip(&headers, remote_addr);
let trusted_proxies = TrustedProxies::new(vec!["192.168.1.1/32".to_string()], true, 5);
let peer_addr: IpAddr = "192.168.1.1".parse().unwrap();
let result = get_source_ip(&headers, peer_addr, &trusted_proxies);
assert_eq!(result, "[2001:db8::1]");
}
#[test]
fn test_is_valid_client_ip() {
// Valid public IPs
assert!(is_valid_client_ip("203.0.113.1", 5));
assert!(is_valid_client_ip("2001:db8::1", 5));
// Invalid private IPs
assert!(!is_valid_client_ip("192.168.1.1", 5));
assert!(!is_valid_client_ip("10.0.0.1", 5));
assert!(!is_valid_client_ip("127.0.0.1", 5));
// Valid chain
assert!(is_valid_client_ip("203.0.113.1, 198.51.100.1", 5));
// Invalid chain (too long)
assert!(!is_valid_client_ip(
"203.0.113.1, 198.51.100.1, 192.0.2.1, 192.0.2.2, 192.0.2.3, 192.0.2.4",
5
));
}
}

View File

@@ -329,14 +329,14 @@ async fn run(opt: config::Opt) -> Result<()> {
init_update_check();
info!(target: "rustfs::main::run","server started successfully at {}", &server_address);
// 4. Mark as Full Ready now that critical components are warm
readiness.mark_stage(SystemStage::FullReady);
println!(
"RustFS server started successfully at {}, current time: {}",
&server_address,
chrono::offset::Utc::now().to_string()
);
info!(target: "rustfs::main::run","server started successfully at {}", &server_address);
// 4. Mark as Full Ready now that critical components are warm
readiness.mark_stage(SystemStage::FullReady);
// Perform hibernation for 1 second
tokio::time::sleep(SHUTDOWN_TIMEOUT).await;