fix(security): harden proxy auth and default credentials (#2981)

* fix(security): harden proxy auth and default credentials

* fix(security): address proxy and credential feedback
This commit is contained in:
安正超
2026-05-16 12:01:50 +08:00
committed by GitHub
parent 824c4f7673
commit 6898e720dd
21 changed files with 374 additions and 115 deletions
+6
View File
@@ -155,6 +155,12 @@ pub const ENV_RUSTFS_SECRET_KEY: &str = "RUSTFS_SECRET_KEY";
/// Environment variable for server secret key file.
pub const ENV_RUSTFS_SECRET_KEY_FILE: &str = "RUSTFS_SECRET_KEY_FILE";
/// Environment variable to explicitly allow public default root credentials.
///
/// This is intended for local development only. Production startup paths should
/// provide non-default `RUSTFS_ACCESS_KEY` and `RUSTFS_SECRET_KEY` values.
pub const ENV_RUSTFS_ALLOW_INSECURE_DEFAULT_CREDENTIALS: &str = "RUSTFS_ALLOW_INSECURE_DEFAULT_CREDENTIALS";
/// Environment variable for server OBS endpoint.
pub const ENV_RUSTFS_OBS_ENDPOINT: &str = "RUSTFS_OBS_ENDPOINT";
+131 -5
View File
@@ -37,6 +37,9 @@ pub const SIMPLE_INTERNAL_ONLY_DEFAULT: bool = true;
const HEADER_FORWARDED: &str = "forwarded";
const HEADER_X_FORWARDED_FOR: &str = "x-forwarded-for";
const HEADER_X_FORWARDED_HOST: &str = "x-forwarded-host";
const HEADER_X_FORWARDED_PROTO: &str = "x-forwarded-proto";
const HEADER_X_FORWARDED_SCHEME: &str = "x-forwarded-scheme";
const HEADER_X_REAL_IP: &str = "x-real-ip";
static ENABLED: OnceLock<bool> = OnceLock::new();
@@ -257,9 +260,15 @@ fn resolve_client_info(peer_addr: Option<SocketAddr>, headers: &HeaderMap) -> Cl
}
match forwarded_client_ip(headers) {
Some(real_ip) if is_usable_ip(real_ip) && real_ip != peer_addr.ip() => {
ClientInfo::from_trusted_proxy(real_ip, None, None, peer_addr.ip(), 1, ValidationMode::Lenient, Vec::new())
}
Some(real_ip) if is_usable_ip(real_ip) && real_ip != peer_addr.ip() => ClientInfo::from_trusted_proxy(
real_ip,
forwarded_host(headers),
forwarded_proto(headers),
peer_addr.ip(),
1,
ValidationMode::Lenient,
Vec::new(),
),
_ => ClientInfo::direct(peer_addr),
}
}
@@ -270,6 +279,72 @@ fn forwarded_client_ip(headers: &HeaderMap) -> Option<IpAddr> {
.or_else(|| parse_forwarded_header(headers))
}
fn forwarded_host(headers: &HeaderMap) -> Option<String> {
parse_single_value_header(headers, HEADER_X_FORWARDED_HOST)
.and_then(sanitize_forwarded_host)
.or_else(|| parse_forwarded_header_value(headers, "host").and_then(sanitize_forwarded_host))
}
fn forwarded_proto(headers: &HeaderMap) -> Option<String> {
parse_single_value_header(headers, HEADER_X_FORWARDED_PROTO)
.and_then(sanitize_forwarded_proto)
.or_else(|| parse_single_value_header(headers, HEADER_X_FORWARDED_SCHEME).and_then(sanitize_forwarded_proto))
.or_else(|| parse_forwarded_header_value(headers, "proto").and_then(sanitize_forwarded_proto))
}
fn parse_single_value_header(headers: &HeaderMap, name: &str) -> Option<String> {
headers
.get(name)?
.to_str()
.ok()?
.split(',')
.next()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
fn parse_forwarded_header_value(headers: &HeaderMap, name: &str) -> Option<String> {
let value = headers.get(HEADER_FORWARDED)?.to_str().ok()?;
let first = value.split(',').next()?.trim();
for part in first.split(';') {
let Some((key, value)) = part.trim().split_once('=') else {
continue;
};
if key.trim().eq_ignore_ascii_case(name) {
let value = value.trim().trim_matches('"');
if !value.is_empty() {
return Some(value.to_string());
}
}
}
None
}
fn sanitize_forwarded_host(value: String) -> Option<String> {
sanitize_forwarded_value(&value).filter(|value| !value.chars().any(char::is_whitespace))
}
fn sanitize_forwarded_proto(value: String) -> Option<String> {
let value = sanitize_forwarded_value(&value)?;
if value.eq_ignore_ascii_case("http") || value.eq_ignore_ascii_case("https") {
return Some(value.to_ascii_lowercase());
}
None
}
fn sanitize_forwarded_value(value: &str) -> Option<String> {
let value = value.trim().trim_matches('"');
if value.is_empty() || value.len() > 256 || value.chars().any(char::is_control) {
return None;
}
Some(value.to_string())
}
fn parse_x_forwarded_for(headers: &HeaderMap) -> Option<IpAddr> {
let value = headers.get(HEADER_X_FORWARDED_FOR)?.to_str().ok()?;
let first = value.split(',').next()?.trim();
@@ -345,8 +420,10 @@ fn parse_implementation(value: Option<&str>) -> TrustedProxyImplementation {
#[cfg(test)]
mod tests {
use super::{
ENV_TRUSTED_PROXY_IMPLEMENTATION, HEADER_FORWARDED, HEADER_X_FORWARDED_FOR, HEADER_X_REAL_IP, TrustedProxyImplementation,
TrustedProxyLayer, forwarded_client_ip, is_internal_ip, parse_implementation, parse_ip_token, resolve_client_info,
ENV_TRUSTED_PROXY_IMPLEMENTATION, HEADER_FORWARDED, HEADER_X_FORWARDED_FOR, HEADER_X_FORWARDED_HOST,
HEADER_X_FORWARDED_PROTO, HEADER_X_FORWARDED_SCHEME, HEADER_X_REAL_IP, TrustedProxyImplementation, TrustedProxyLayer,
forwarded_client_ip, forwarded_host, forwarded_proto, is_internal_ip, parse_implementation, parse_ip_token,
resolve_client_info,
};
use crate::ClientInfo;
use axum::http::{HeaderMap, HeaderValue};
@@ -403,6 +480,55 @@ mod tests {
assert_eq!(client_info.proxy_ip, Some(IpAddr::from([10, 0, 0, 5])));
}
#[test]
fn test_internal_peer_preserves_forwarded_proto_and_host() {
let mut headers = HeaderMap::new();
headers.insert(HEADER_X_FORWARDED_FOR, HeaderValue::from_static("203.0.113.10"));
headers.insert(HEADER_X_FORWARDED_HOST, HeaderValue::from_static("s3.example.test"));
headers.insert(HEADER_X_FORWARDED_PROTO, HeaderValue::from_static("https"));
let client_info = resolve_client_info(Some(SocketAddr::from(([10, 0, 0, 5], 9000))), &headers);
assert_eq!(client_info.forwarded_host.as_deref(), Some("s3.example.test"));
assert_eq!(client_info.forwarded_proto.as_deref(), Some("https"));
}
#[test]
fn test_internal_peer_preserves_forwarded_scheme_fallback() {
let mut headers = HeaderMap::new();
headers.insert(HEADER_X_FORWARDED_FOR, HeaderValue::from_static("203.0.113.10"));
headers.insert(HEADER_X_FORWARDED_SCHEME, HeaderValue::from_static("https"));
let client_info = resolve_client_info(Some(SocketAddr::from(([10, 0, 0, 5], 9000))), &headers);
assert_eq!(client_info.forwarded_proto.as_deref(), Some("https"));
}
#[test]
fn test_forwarded_header_preserves_proto_and_host() {
let mut headers = HeaderMap::new();
headers.insert(
HEADER_FORWARDED,
HeaderValue::from_static("for=203.0.113.10;proto=https;host=s3.example.test"),
);
let client_info = resolve_client_info(Some(SocketAddr::from(([10, 0, 0, 5], 9000))), &headers);
assert_eq!(client_info.real_ip, IpAddr::from([203, 0, 113, 10]));
assert_eq!(client_info.forwarded_host.as_deref(), Some("s3.example.test"));
assert_eq!(client_info.forwarded_proto.as_deref(), Some("https"));
}
#[test]
fn test_invalid_forwarded_host_and_proto_are_ignored() {
let mut headers = HeaderMap::new();
headers.insert(HEADER_X_FORWARDED_HOST, HeaderValue::from_static("bad host"));
headers.insert(HEADER_X_FORWARDED_PROTO, HeaderValue::from_static("ftp"));
assert_eq!(forwarded_host(&headers), None);
assert_eq!(forwarded_proto(&headers), None);
}
#[test]
fn test_public_peer_keeps_direct_ip() {
let mut headers = HeaderMap::new();