diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index cee14dc4b..ccead9e89 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -5,6 +5,7 @@ - On Windows, an error is now returned from `HttpServer::bind()` (or TLS variants) when binding to a socket that's already in use. - Update `brotli` dependency to `7`. - Minimum supported Rust version (MSRV) is now 1.75. +- Add trusted proxies features to allow for customizing the list of trusted proxies and headers for determining host, scheme and ip of the `ConnectionInfo` object. ## 4.9.0 diff --git a/actix-web/Cargo.toml b/actix-web/Cargo.toml index 9b10f14b1..6cc956b29 100644 --- a/actix-web/Cargo.toml +++ b/actix-web/Cargo.toml @@ -152,6 +152,7 @@ futures-core = { version = "0.3.17", default-features = false } futures-util = { version = "0.3.17", default-features = false } itoa = "1" impl-more = "0.1.4" +ipnet = "2.10.1" language-tags = "0.3" log = "0.4" mime = "0.3" diff --git a/actix-web/src/config.rs b/actix-web/src/config.rs index 0e856f574..9fdeee271 100644 --- a/actix-web/src/config.rs +++ b/actix-web/src/config.rs @@ -14,6 +14,7 @@ use crate::{ AppServiceFactory, BoxedHttpServiceFactory, HttpServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse, }, + trusted_proxies::TrustedProxies, }; type Guards = Vec>; @@ -112,17 +113,28 @@ pub struct AppConfig { secure: bool, host: String, addr: SocketAddr, + trusted_proxies: TrustedProxies, } impl AppConfig { - pub(crate) fn new(secure: bool, host: String, addr: SocketAddr) -> Self { - AppConfig { secure, host, addr } + pub(crate) fn new( + secure: bool, + host: String, + addr: SocketAddr, + trusted_proxies: TrustedProxies, + ) -> Self { + AppConfig { + secure, + host, + addr, + trusted_proxies, + } } /// Needed in actix-test crate. Semver exempt. #[doc(hidden)] pub fn __priv_test_new(secure: bool, host: String, addr: SocketAddr) -> Self { - AppConfig::new(secure, host, addr) + AppConfig::new(secure, host, addr, TrustedProxies::default()) } /// Server host name. @@ -146,6 +158,17 @@ impl AppConfig { self.addr } + /// Returns the trusted proxies list. + pub fn trusted_proxies(&self) -> &TrustedProxies { + &self.trusted_proxies + } + + /// Set the trusted proxies + #[cfg(test)] + pub fn set_trusted_proxies(&mut self, proxies: TrustedProxies) { + self.trusted_proxies = proxies; + } + #[cfg(test)] pub(crate) fn set_host(&mut self, host: &str) { host.clone_into(&mut self.host); @@ -168,6 +191,7 @@ impl Default for AppConfig { false, "localhost:8080".to_owned(), "127.0.0.1:8080".parse().unwrap(), + TrustedProxies::default(), ) } } diff --git a/actix-web/src/info.rs b/actix-web/src/info.rs index 0655a3df2..5eebfbd9b 100644 --- a/actix-web/src/info.rs +++ b/actix-web/src/info.rs @@ -1,4 +1,7 @@ -use std::{convert::Infallible, net::SocketAddr}; +use std::{ + convert::Infallible, + net::{IpAddr, SocketAddr}, +}; use actix_utils::future::{err, ok, Ready}; use derive_more::derive::{Display, Error}; @@ -6,16 +9,12 @@ use derive_more::derive::{Display, Error}; use crate::{ dev::{AppConfig, Payload, RequestHead}, http::{ - header::{self, HeaderName}, + header, uri::{Authority, Scheme}, }, FromRequest, HttpRequest, ResponseError, }; -static X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for"); -static X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host"); -static X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto"); - /// Trim whitespace then any quote marks. fn unquote(val: &str) -> &str { val.trim().trim_start_matches('"').trim_end_matches('"') @@ -35,11 +34,25 @@ fn bare_address(val: &str) -> &str { } } -/// Extracts and trims first value for given header name. -fn first_header_value<'a>(req: &'a RequestHead, name: &'_ HeaderName) -> Option<&'a str> { - let hdr = req.headers.get(name)?.to_str().ok()?; - let val = hdr.split(',').next()?.trim(); - Some(val) +/// Extract default host from request or server configuration. +fn default_host<'a>(req: &'a RequestHead, cfg: &'a AppConfig) -> &'a str { + req.headers + .get(&header::HOST) + .and_then(|v| v.to_str().ok()) + // skip host header if HTTP/2, we should use :authority instead + .filter(|_| req.version < actix_http::Version::HTTP_2) + .or_else(|| req.uri.authority().map(Authority::as_str)) + // @TODO can we get the sni host if in secure context ? + .unwrap_or_else(|| cfg.host()) +} + +/// Extract default scheme from request or server configuration. +fn default_scheme<'a>(req: &'a RequestHead, cfg: &'a AppConfig) -> &'a str { + req.uri + .scheme() + .map(Scheme::as_str) + .or_else(|| Some("https").filter(|_| cfg.secure())) + .unwrap_or("http") } /// HTTP connection information. @@ -70,6 +83,9 @@ fn first_header_value<'a>(req: &'a RequestHead, name: &'_ HeaderName) -> Option< /// If the older, related headers are also present (eg. `X-Forwarded-For`), then `Forwarded` /// is preferred. /// +/// Header are parsed only if the peer address is trusted and the header is trusted, otherwise the +/// request is considered to be direct and the headers are ignored. +/// /// [rfc7239]: https://datatracker.ietf.org/doc/html/rfc7239 /// [rfc7239-62]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.2 /// [rfc7239-63]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.3 @@ -83,67 +99,151 @@ pub struct ConnectionInfo { impl ConnectionInfo { pub(crate) fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo { - let mut host = None; - let mut scheme = None; - let mut realip_remote_addr = None; + let (host, scheme, peer_addr, realip_remote_addr) = + match req.peer_addr.map(|addr| addr.ip()) { + // since we don't have a peer address, we can't determine the real IP and we cannot trust any headers + // set the host and scheme to the server's configuration + None => ( + default_host(req, cfg).to_string(), + default_scheme(req, cfg).to_string(), + None, + None, + ), + Some(ip) => { + if !cfg.trusted_proxies().trust_ip(&ip) { + // if the peer address is not trusted, we can't trust the headers + // set the host and scheme to the server's configuration + ( + default_host(req, cfg).to_string(), + default_scheme(req, cfg).to_string(), + Some(ip.to_string()), + None, + ) + } else { + // if the peer address is trusted, we can start to check trusted header to get correct information - for (name, val) in req - .headers - .get_all(&header::FORWARDED) - .filter_map(|hdr| hdr.to_str().ok()) - // "for=1.2.3.4, for=5.6.7.8; scheme=https" - .flat_map(|val| val.split(';')) - // ["for=1.2.3.4, for=5.6.7.8", " scheme=https"] - .flat_map(|vals| vals.split(',')) - // ["for=1.2.3.4", " for=5.6.7.8", " scheme=https"] - .flat_map(|pair| { - let mut items = pair.trim().splitn(2, '='); - Some((items.next()?, items.next()?)) - }) - { - // [(name , val ), ... ] - // [("for", "1.2.3.4"), ("for", "5.6.7.8"), ("scheme", "https")] + let mut host = None; + let mut scheme = None; + let mut realip_remote_addr = None; - // taking the first value for each property is correct because spec states that first - // "for" value is client and rest are proxies; multiple values other properties have - // no defined semantics - // - // > In a chain of proxy servers where this is fully utilized, the first - // > "for" parameter will disclose the client where the request was first - // > made, followed by any subsequent proxy identifiers. - // --- https://datatracker.ietf.org/doc/html/rfc7239#section-5.2 + // first check the forwarded header if it is trusted + if cfg.trusted_proxies().trust_header(&header::FORWARDED) { + // quote from RFC 7239: + // A proxy server that wants to add a new "Forwarded" header field value + // can either append it to the last existing "Forwarded" header field + // after a comma separator or add a new field at the end of the header + // block. + // --- https://datatracker.ietf.org/doc/html/rfc7239#section-4 + // so we get the values in reverse order as we want to get the first untrusted value + let forwarded_list = req + .headers + .get_all(&header::FORWARDED) + .filter_map(|hdr| hdr.to_str().ok()) + // "for=1.2.3.4, for=5.6.7.8; scheme=https" + .flat_map(|vals| vals.split(',')) + // ["for=1.2.3.4", "for=5.6.7.8; scheme=https"] + .rev(); - match name.trim().to_lowercase().as_str() { - "for" => realip_remote_addr.get_or_insert_with(|| bare_address(unquote(val))), - "proto" => scheme.get_or_insert_with(|| unquote(val)), - "host" => host.get_or_insert_with(|| unquote(val)), - "by" => { - // TODO: implement https://datatracker.ietf.org/doc/html/rfc7239#section-5.1 - continue; + 'forwaded: for forwarded in forwarded_list { + for (key, value) in forwarded.split(';').map(|item| { + let mut kv = item.splitn(2, '='); + + ( + kv.next().map(|s| s.trim()).unwrap_or_default(), + kv.next().map(|s| unquote(s.trim())).unwrap_or_default(), + ) + }) { + match key.to_lowercase().as_str() { + "for" => { + if let Ok(ip) = bare_address(value).parse::() { + if cfg.trusted_proxies().trust_ip(&ip) { + host = None; + scheme = None; + realip_remote_addr = None; + + continue 'forwaded; + } + } + + realip_remote_addr = Some(bare_address(value)); + } + "proto" => { + scheme = Some(value); + } + "host" => { + host = Some(value); + } + "by" => { + // TODO: implement https://datatracker.ietf.org/doc/html/rfc7239#section-5.1 + } + _ => {} + } + } + + break 'forwaded; + } + } + + if realip_remote_addr.is_none() + && cfg.trusted_proxies().trust_header(&header::X_FORWARDED_FOR) + { + for value in req + .headers + .get_all(&header::X_FORWARDED_FOR) + .filter_map(|hdr| hdr.to_str().ok()) + .flat_map(|vals| vals.split(',')) + .rev() + { + if let Ok(ip) = bare_address(value).parse::() { + if cfg.trusted_proxies().trust_ip(&ip) { + continue; + } + } + + realip_remote_addr = Some(bare_address(value)); + break; + } + } + + if host.is_none() + && cfg + .trusted_proxies() + .trust_header(&header::X_FORWARDED_HOST) + { + host = req + .headers + .get_all(&header::X_FORWARDED_HOST) + .filter_map(|hdr| hdr.to_str().ok()) + .flat_map(|vals| vals.split(',')) + .rev() + .next(); + } + + if scheme.is_none() + && cfg + .trusted_proxies() + .trust_header(&header::X_FORWARDED_PROTO) + { + scheme = req + .headers + .get_all(&header::X_FORWARDED_PROTO) + .filter_map(|hdr| hdr.to_str().ok()) + .flat_map(|vals| vals.split(',')) + .rev() + .next(); + } + + ( + host.unwrap_or_else(|| default_host(req, cfg)).to_string(), + scheme + .unwrap_or_else(|| default_scheme(req, cfg)) + .to_string(), + Some(ip.to_string()), + realip_remote_addr.map(|s| s.to_string()), + ) + } } - _ => continue, }; - } - - let scheme = scheme - .or_else(|| first_header_value(req, &X_FORWARDED_PROTO)) - .or_else(|| req.uri.scheme().map(Scheme::as_str)) - .or_else(|| Some("https").filter(|_| cfg.secure())) - .unwrap_or("http") - .to_owned(); - - let host = host - .or_else(|| first_header_value(req, &X_FORWARDED_HOST)) - .or_else(|| req.headers.get(&header::HOST)?.to_str().ok()) - .or_else(|| req.uri.authority().map(Authority::as_str)) - .unwrap_or_else(|| cfg.host()) - .to_owned(); - - let realip_remote_addr = realip_remote_addr - .or_else(|| first_header_value(req, &X_FORWARDED_FOR)) - .map(str::to_owned); - - let peer_addr = req.peer_addr.map(|addr| addr.ip().to_string()); ConnectionInfo { host, @@ -270,7 +370,7 @@ impl FromRequest for PeerAddr { #[cfg(test)] mod tests { use super::*; - use crate::test::TestRequest; + use crate::{test::TestRequest, trusted_proxies::TrustedProxies}; const X_FORWARDED_FOR: &str = "x-forwarded-for"; const X_FORWARDED_HOST: &str = "x-forwarded-host"; @@ -297,8 +397,15 @@ mod tests { } #[test] - fn x_forwarded_for_header() { + fn x_forwarded_for_header_trusted() { + let mut trusted_proxies = TrustedProxies::new_local(); + trusted_proxies.add_trusted_header(header::X_FORWARDED_FOR); + + let addr = "127.0.0.1:8080".parse().unwrap(); + let req = TestRequest::default() + .peer_addr(addr) + .set_trusted_proxies(trusted_proxies) .insert_header((X_FORWARDED_FOR, "192.0.2.60")) .to_http_request(); let info = req.connection_info(); @@ -306,18 +413,77 @@ mod tests { } #[test] - fn x_forwarded_host_header() { + fn x_forwarded_for_header_trusted_multiple() { + let mut trusted_proxies = TrustedProxies::new_local(); + trusted_proxies + .add_trusted_proxy("192.0.2.60") + .expect("failed to add trusted proxy"); + trusted_proxies.add_trusted_header(header::X_FORWARDED_FOR); + + let addr = "127.0.0.1:8080".parse().unwrap(); + let req = TestRequest::default() + .peer_addr(addr) + .set_trusted_proxies(trusted_proxies) + .append_header((X_FORWARDED_FOR, "240.10.56.47")) + .append_header((X_FORWARDED_FOR, "192.0.2.60")) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.realip_remote_addr(), Some("240.10.56.47")); + } + + #[test] + fn x_forwarded_for_header_untrusted() { + let addr = "127.0.0.1:8080".parse().unwrap(); + + let req = TestRequest::default() + .peer_addr(addr) + .insert_header((X_FORWARDED_FOR, "192.0.2.60")) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.realip_remote_addr(), Some("127.0.0.1")); + } + + #[test] + fn x_forwarded_host_header_trusted() { + let mut trusted_proxies = TrustedProxies::new_local(); + trusted_proxies.add_trusted_header(header::X_FORWARDED_HOST); + + let addr = "127.0.0.1:8080".parse().unwrap(); + + let req = TestRequest::default() + .peer_addr(addr) + .set_trusted_proxies(trusted_proxies) .insert_header((X_FORWARDED_HOST, "192.0.2.60")) .to_http_request(); let info = req.connection_info(); assert_eq!(info.host(), "192.0.2.60"); - assert_eq!(info.realip_remote_addr(), None); + assert_eq!(info.realip_remote_addr(), Some("127.0.0.1")); } #[test] - fn x_forwarded_proto_header() { + fn x_forwarded_host_header_untrusted() { + let addr = "127.0.0.1:8080".parse().unwrap(); + let req = TestRequest::default() + .peer_addr(addr) + .insert_header((X_FORWARDED_HOST, "192.0.2.60")) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.host(), "localhost:8080"); + assert_eq!(info.realip_remote_addr(), Some("127.0.0.1")); + } + + #[test] + fn x_forwarded_proto_header_trusted() { + let mut trusted_proxies = TrustedProxies::new_local(); + trusted_proxies.add_trusted_header(header::X_FORWARDED_PROTO); + + let addr = "127.0.0.1:8080".parse().unwrap(); + + let req = TestRequest::default() + .peer_addr(addr) + .set_trusted_proxies(trusted_proxies) .insert_header((X_FORWARDED_PROTO, "https")) .to_http_request(); let info = req.connection_info(); @@ -325,8 +491,21 @@ mod tests { } #[test] - fn forwarded_header() { + fn x_forwarded_proto_header_untrusted() { + let addr = "127.0.0.1:8080".parse().unwrap(); let req = TestRequest::default() + .peer_addr(addr) + .insert_header((X_FORWARDED_PROTO, "https")) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.scheme(), "http"); + } + + #[test] + fn forwarded_header() { + let addr = "127.0.0.1:8080".parse().unwrap(); + let req = TestRequest::default() + .peer_addr(addr) .insert_header(( header::FORWARDED, "for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org", @@ -339,6 +518,7 @@ mod tests { assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); let req = TestRequest::default() + .peer_addr(addr) .insert_header(( header::FORWARDED, "for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org", @@ -353,7 +533,9 @@ mod tests { #[test] fn forwarded_case_sensitivity() { + let addr = "127.0.0.1:8080".parse().unwrap(); let req = TestRequest::default() + .peer_addr(addr) .insert_header((header::FORWARDED, "For=192.0.2.60")) .to_http_request(); let info = req.connection_info(); @@ -362,7 +544,9 @@ mod tests { #[test] fn forwarded_weird_whitespace() { + let addr = "127.0.0.1:8080".parse().unwrap(); let req = TestRequest::default() + .peer_addr(addr) .insert_header((header::FORWARDED, "for= 1.2.3.4; proto= https")) .to_http_request(); let info = req.connection_info(); @@ -370,6 +554,7 @@ mod tests { assert_eq!(info.scheme(), "https"); let req = TestRequest::default() + .peer_addr(addr) .insert_header((header::FORWARDED, " for = 1.2.3.4 ")) .to_http_request(); let info = req.connection_info(); @@ -378,7 +563,9 @@ mod tests { #[test] fn forwarded_for_quoted() { + let addr = "127.0.0.1:8080".parse().unwrap(); let req = TestRequest::default() + .peer_addr(addr) .insert_header((header::FORWARDED, r#"for="192.0.2.60:8080""#)) .to_http_request(); let info = req.connection_info(); @@ -387,7 +574,9 @@ mod tests { #[test] fn forwarded_for_ipv6() { + let addr = "127.0.0.1:8080".parse().unwrap(); let req = TestRequest::default() + .peer_addr(addr) .insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]""#)) .to_http_request(); let info = req.connection_info(); @@ -396,7 +585,9 @@ mod tests { #[test] fn forwarded_for_ipv6_with_port() { + let addr = "127.0.0.1:8080".parse().unwrap(); let req = TestRequest::default() + .peer_addr(addr) .insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]:4711""#)) .to_http_request(); let info = req.connection_info(); @@ -405,11 +596,29 @@ mod tests { #[test] fn forwarded_for_multiple() { + let addr = "127.0.0.1:8080".parse().unwrap(); let req = TestRequest::default() + .peer_addr(addr) .insert_header((header::FORWARDED, "for=192.0.2.60, for=198.51.100.17")) .to_http_request(); let info = req.connection_info(); - // takes the first value + + // takes the last untrusted value + assert_eq!(info.realip_remote_addr(), Some("198.51.100.17")); + + let mut trusted_proxies = TrustedProxies::new_local(); + trusted_proxies + .add_trusted_proxy("198.51.100.17") + .expect("Failed to add trusted proxy"); + + let req = TestRequest::default() + .set_trusted_proxies(trusted_proxies) + .peer_addr(addr) + .insert_header((header::FORWARDED, "for=192.0.2.60, for=198.51.100.17")) + .to_http_request(); + let info = req.connection_info(); + + // takes the last untrusted value assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); } diff --git a/actix-web/src/lib.rs b/actix-web/src/lib.rs index e2a8e2275..a95c76e27 100644 --- a/actix-web/src/lib.rs +++ b/actix-web/src/lib.rs @@ -105,6 +105,7 @@ mod server; mod service; pub mod test; mod thin_data; +pub mod trusted_proxies; pub(crate) mod types; pub mod web; diff --git a/actix-web/src/middleware/logger.rs b/actix-web/src/middleware/logger.rs index 21986baae..87734f928 100644 --- a/actix-web/src/middleware/logger.rs +++ b/actix-web/src/middleware/logger.rs @@ -898,8 +898,10 @@ mod tests { #[actix_rt::test] async fn test_remote_addr_format() { let mut format = Format::new("%{r}a"); + let addr = "127.0.0.1:8080".parse().unwrap(); let req = TestRequest::default() + .peer_addr(addr) .insert_header(( header::FORWARDED, header::HeaderValue::from_static("for=192.0.2.60;proto=http;by=203.0.113.43"), diff --git a/actix-web/src/server.rs b/actix-web/src/server.rs index 1ea4de4ca..d89e5bfee 100644 --- a/actix-web/src/server.rs +++ b/actix-web/src/server.rs @@ -17,7 +17,7 @@ use actix_service::{ #[cfg(feature = "openssl")] use actix_tls::accept::openssl::reexports::{AlpnError, SslAcceptor, SslAcceptorBuilder}; -use crate::{config::AppConfig, Error}; +use crate::{config::AppConfig, trusted_proxies::TrustedProxies, Error}; struct Socket { scheme: &'static str, @@ -26,6 +26,7 @@ struct Socket { struct Config { host: Option, + trusted_proxies: TrustedProxies, keep_alive: KeepAlive, client_request_timeout: Duration, client_disconnect_timeout: Duration, @@ -110,6 +111,7 @@ where factory, config: Arc::new(Mutex::new(Config { host: None, + trusted_proxies: TrustedProxies::new_local(), keep_alive: KeepAlive::default(), client_request_timeout: Duration::from_secs(5), client_disconnect_timeout: Duration::from_secs(1), @@ -142,6 +144,14 @@ where self } + /// Sets server trusted proxies configuration. + /// + /// By default server trusts loopback, link-local, and private networks and Forwarded Header. + pub fn trusted_proxies(self, val: TrustedProxies) -> Self { + self.config.lock().unwrap().trusted_proxies = val; + self + } + /// Sets server keep-alive preference. /// /// By default keep-alive is set to 5 seconds. @@ -526,6 +536,7 @@ where .listen(format!("actix-web-service-{}", addr), lst, move || { let cfg = cfg.lock().unwrap(); let host = cfg.host.clone().unwrap_or_else(|| format!("{}", addr)); + let trusted_proxies = cfg.trusted_proxies.clone(); let mut svc = HttpService::build() .keep_alive(cfg.keep_alive) @@ -543,7 +554,7 @@ where .map_err(|err| err.into().error_response()); svc.finish(map_config(fac, move |_| { - AppConfig::new(false, host.clone(), addr) + AppConfig::new(false, host.clone(), addr, trusted_proxies.clone()) })) .tcp() })?; @@ -570,6 +581,7 @@ where .listen(format!("actix-web-service-{}", addr), lst, move || { let cfg = cfg.lock().unwrap(); let host = cfg.host.clone().unwrap_or_else(|| format!("{}", addr)); + let trusted_proxies = cfg.trusted_proxies.clone(); let mut svc = HttpService::build() .keep_alive(cfg.keep_alive) @@ -587,7 +599,7 @@ where .map_err(|err| err.into().error_response()); svc.finish(map_config(fac, move |_| { - AppConfig::new(false, host.clone(), addr) + AppConfig::new(false, host.clone(), addr, trusted_proxies.clone()) })) .tcp_auto_h2c() })?; @@ -646,6 +658,7 @@ where .listen(format!("actix-web-service-{}", addr), lst, move || { let c = cfg.lock().unwrap(); let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); + let trusted_proxies = c.trusted_proxies.clone(); let svc = HttpService::build() .keep_alive(c.keep_alive) @@ -668,7 +681,7 @@ where }; svc.finish(map_config(fac, move |_| { - AppConfig::new(true, host.clone(), addr) + AppConfig::new(true, host.clone(), addr, trusted_proxies.clone()) })) .rustls_with_config(config.clone(), acceptor_config) })?; @@ -697,6 +710,7 @@ where .listen(format!("actix-web-service-{}", addr), lst, move || { let c = cfg.lock().unwrap(); let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); + let trusted_proxies = c.trusted_proxies.clone(); let svc = HttpService::build() .keep_alive(c.keep_alive) @@ -719,7 +733,7 @@ where }; svc.finish(map_config(fac, move |_| { - AppConfig::new(true, host.clone(), addr) + AppConfig::new(true, host.clone(), addr, trusted_proxies.clone()) })) .rustls_021_with_config(config.clone(), acceptor_config) })?; @@ -763,6 +777,7 @@ where .listen(format!("actix-web-service-{}", addr), lst, move || { let c = cfg.lock().unwrap(); let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); + let trusted_proxies = c.trusted_proxies.clone(); let svc = HttpService::build() .keep_alive(c.keep_alive) @@ -785,7 +800,7 @@ where }; svc.finish(map_config(fac, move |_| { - AppConfig::new(true, host.clone(), addr) + AppConfig::new(true, host.clone(), addr, trusted_proxies.clone()) })) .rustls_0_22_with_config(config.clone(), acceptor_config) })?; @@ -829,6 +844,7 @@ where .listen(format!("actix-web-service-{}", addr), lst, move || { let c = cfg.lock().unwrap(); let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); + let trusted_proxies = c.trusted_proxies.clone(); let svc = HttpService::build() .keep_alive(c.keep_alive) @@ -851,7 +867,7 @@ where }; svc.finish(map_config(fac, move |_| { - AppConfig::new(true, host.clone(), addr) + AppConfig::new(true, host.clone(), addr, trusted_proxies.clone()) })) .rustls_0_23_with_config(config.clone(), acceptor_config) })?; @@ -894,6 +910,7 @@ where .listen(format!("actix-web-service-{}", addr), lst, move || { let c = cfg.lock().unwrap(); let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); + let trusted_proxies = c.trusted_proxies.clone(); let svc = HttpService::build() .keep_alive(c.keep_alive) @@ -919,7 +936,7 @@ where }; svc.finish(map_config(fac, move |_| { - AppConfig::new(true, host.clone(), addr) + AppConfig::new(true, host.clone(), addr, trusted_proxies.clone()) })) .openssl_with_config(acceptor.clone(), acceptor_config) })?; @@ -956,6 +973,7 @@ where false, c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)), socket_addr, + c.trusted_proxies.clone(), ); let fac = factory() @@ -1001,6 +1019,7 @@ where false, c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)), socket_addr, + c.trusted_proxies.clone(), ); fn_service(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) }).and_then({ diff --git a/actix-web/src/test/test_request.rs b/actix-web/src/test/test_request.rs index f178d6f43..05384b780 100644 --- a/actix-web/src/test/test_request.rs +++ b/actix-web/src/test/test_request.rs @@ -5,6 +5,8 @@ use serde::Serialize; #[cfg(feature = "cookies")] use crate::cookie::{Cookie, CookieJar}; +#[cfg(test)] +use crate::trusted_proxies::TrustedProxies; use crate::{ app_service::AppInitServiceState, config::AppConfig, @@ -229,6 +231,13 @@ impl TestRequest { self } + /// Set the trusted proxies for this request. + #[cfg(test)] + pub fn set_trusted_proxies(mut self, trusted_proxies: TrustedProxies) -> Self { + self.config.set_trusted_proxies(trusted_proxies); + self + } + /// Finalizes test request. /// /// This request builder will be useless after calling `finish()`. diff --git a/actix-web/src/trusted_proxies.rs b/actix-web/src/trusted_proxies.rs new file mode 100644 index 000000000..18a804aa8 --- /dev/null +++ b/actix-web/src/trusted_proxies.rs @@ -0,0 +1,106 @@ +use std::net::IpAddr; + +use actix_http::header::{HeaderName, FORWARDED}; +use ipnet::{AddrParseError, IpNet}; + +/// TrustedProxies is a helper struct to manage trusted proxies and headers +/// +/// This is used to determine if information from a request can be trusted or not. +/// +/// By default, it trusts the following: +/// - IPV4 Loopback +/// - IPV4 Private Networks +/// - IPV6 Loopback +/// - IPV6 Private Networks +/// +/// It also trusts the `FORWARDED` header by default. +/// +/// # Example +/// ``` +/// use actix_web::trusted_proxies::TrustedProxies; +/// use actix_web::{web, App, HttpResponse, HttpServer}; +/// +/// let mut trusted_proxies = TrustedProxies::new_local(); +/// trusted_proxies.add_trusted_proxy("168.10.0.0/16").unwrap(); +/// trusted_proxies.add_trusted_header("X-Forwarded-For".parse().unwrap()); +/// +/// HttpServer::new(|| { +/// App::new() +/// .service(web::resource("/").to(|| async { "hello world" })) +/// }).trusted_proxies(trusted_proxies); +/// ``` +#[derive(Debug, Clone)] +pub struct TrustedProxies(Vec, Vec); + +impl Default for TrustedProxies { + fn default() -> Self { + Self::new_local() + } +} +impl TrustedProxies { + /// Create a new TrustedProxies instance with no trusted proxies or headers + pub fn new() -> Self { + Self(vec![], vec![]) + } + + /// Create a new TrustedProxies instance with local and private networks and FORWARDED header trusted + pub fn new_local() -> Self { + Self( + vec![ + // IPV4 Loopback + "127.0.0.0/8".parse().unwrap(), + // IPV4 Private Networks + "10.0.0.0/8".parse().unwrap(), + "172.16.0.0/12".parse().unwrap(), + "192.168.0.0/16".parse().unwrap(), + // IPV6 Loopback + "::1/128".parse().unwrap(), + // IPV6 Private network + "fd00::/8".parse().unwrap(), + ], + vec![FORWARDED], + ) + } + + /// Add a trusted header to the list of trusted headers + pub fn add_trusted_header(&mut self, header: HeaderName) { + self.1.push(header); + } + + /// Add a trusted proxy to the list of trusted proxies + /// + /// proxy can be an IP address or a CIDR + pub fn add_trusted_proxy(&mut self, proxy: &str) -> Result<(), AddrParseError> { + match proxy.parse() { + Ok(v) => { + self.0.push(v); + + Ok(()) + } + Err(e) => match proxy.parse::() { + Ok(v) => { + self.0.push(IpNet::from(v)); + + Ok(()) + } + _ => Err(e), + }, + } + } + + /// Check if a remote address is trusted given the list of trusted proxies + pub fn trust_ip(&self, remote_addr: &IpAddr) -> bool { + for proxy in &self.0 { + if proxy.contains(remote_addr) { + return true; + } + } + + false + } + + /// Check if a header is trusted + pub fn trust_header(&self, header: &HeaderName) -> bool { + self.1.contains(header) + } +}