From e5f9e2f752d76a487f80bdd1960bb080ccefe369 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 17 Jul 2026 02:04:30 +0000 Subject: [PATCH] fix(security): derive rate-limit client IP from trusted proxy safely When HTTP_TRUSTED_PROXIES is configured, use X-Real-IP or the rightmost X-Forwarded-For hop instead of the client-controlled leftmost entry. This closes a rate-limit bypass and DoS amplification path behind nginx-style reverse proxies that append the connecting address. Co-authored-by: Alexander Wagner --- .env.example | 2 +- src/config.rs | 4 ++- src/rate_limit.rs | 72 ++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 69 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 223cde1..f04ca5e 100644 --- a/.env.example +++ b/.env.example @@ -36,7 +36,7 @@ RTMPS_BIND=0.0.0.0:1936 # HTTP API and UI listener address HTTP_BIND=0.0.0.0:8080 -# Comma-separated proxy IPs that may set X-Forwarded-For for rate limiting +# Comma-separated proxy IPs that may set X-Real-IP / X-Forwarded-For for rate limiting # Example behind Docker/nginx on the same host: 127.0.0.1,172.17.0.1 HTTP_TRUSTED_PROXIES= diff --git a/src/config.rs b/src/config.rs index df4fb7b..d08cf88 100644 --- a/src/config.rs +++ b/src/config.rs @@ -29,7 +29,9 @@ pub struct ServerConfig { /// HTTP API + UI, e.g. "0.0.0.0:8080" pub http_bind: String, - /// When the TCP peer is one of these addresses, use `X-Forwarded-For` for rate limiting. + /// When the TCP peer is one of these addresses, derive the client IP from + /// `X-Real-IP` or the rightmost `X-Forwarded-For` hop (the address appended + /// by the immediate trusted proxy). pub http_trusted_proxies: Vec, /// HTTP rate-limit sliding window (seconds). diff --git a/src/rate_limit.rs b/src/rate_limit.rs index 2b5b306..d787cc5 100644 --- a/src/rate_limit.rs +++ b/src/rate_limit.rs @@ -144,15 +144,36 @@ fn client_ip(request: &Request, trusted_proxies: &[IpAddr]) -> IpAddr { .map(|info| info.0.ip()) .unwrap_or(IpAddr::from([127, 0, 0, 1])); - if trusted_proxies.contains(&peer) - && let Some(xff) = request + if trusted_proxies.contains(&peer) { + // Prefer X-Real-IP when the trusted proxy sets it (e.g. nginx $remote_addr). + if let Some(real_ip) = request + .headers() + .get("X-Real-IP") + .and_then(|v| v.to_str().ok()) + .map(str::trim) + .filter(|part| !part.is_empty()) + .and_then(|part| part.parse::().ok()) + { + return real_ip; + } + + if let Some(xff) = request .headers() .get("X-Forwarded-For") .and_then(|v| v.to_str().ok()) - && let Some(client) = xff.split(',').map(str::trim).find(|part| !part.is_empty()) - && let Ok(ip) = client.parse::() - { - return ip; + { + // Use the rightmost address: the one appended by the immediate trusted + // proxy ($proxy_add_x_forwarded_for), not client-controlled leftmost entries. + if let Some(client) = xff + .split(',') + .map(str::trim) + .filter(|part| !part.is_empty()) + .next_back() + .and_then(|part| part.parse::().ok()) + { + return client; + } + } } peer @@ -287,7 +308,7 @@ mod tests { } #[test] - fn trusted_proxy_uses_x_forwarded_for() { + fn trusted_proxy_uses_rightmost_x_forwarded_for() { use axum::body::Body; let proxy = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)); @@ -300,6 +321,43 @@ mod tests { .extensions_mut() .insert(ConnectInfo(SocketAddr::from(([10, 0, 0, 1], 12345)))); + let ip = client_ip(&request, &[proxy]); + assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))); + } + + #[test] + fn trusted_proxy_ignores_client_supplied_leftmost_xff() { + use axum::body::Body; + + let proxy = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)); + let mut request = Request::builder() + .uri("/api/v1/health") + .header("X-Forwarded-For", "198.51.100.99, 203.0.113.5") + .body(Body::empty()) + .unwrap(); + request + .extensions_mut() + .insert(ConnectInfo(SocketAddr::from(([10, 0, 0, 1], 12345)))); + + let ip = client_ip(&request, &[proxy]); + assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(203, 0, 113, 5))); + } + + #[test] + fn trusted_proxy_prefers_x_real_ip() { + use axum::body::Body; + + let proxy = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)); + let mut request = Request::builder() + .uri("/api/v1/health") + .header("X-Real-IP", "203.0.113.5") + .header("X-Forwarded-For", "198.51.100.99, 10.0.0.1") + .body(Body::empty()) + .unwrap(); + request + .extensions_mut() + .insert(ConnectInfo(SocketAddr::from(([10, 0, 0, 1], 12345)))); + let ip = client_ip(&request, &[proxy]); assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(203, 0, 113, 5))); }