Skip to main content

http_extract/
x_forwarded.rs

1//! Parsing of untrusted `X-Forwarded-*` header conventions.
2//!
3//! These fields are widely deployed de facto conventions, not IETF standards.
4//! The functions here only parse raw assertions; they do not establish trust or
5//! select an effective client IP or request scheme. RFC 7239 `Forwarded` is the
6//! standardized alternative for forwarding information; see
7//! [RFC 7239, Section 4].
8//!
9//! [RFC 7239, Section 4]: https://www.rfc-editor.org/rfc/rfc7239.html#section-4
10
11use std::net::{IpAddr, SocketAddr};
12
13use http::{HeaderMap, HeaderName, Request};
14
15use crate::Error;
16
17/// The de facto, non-IETF `X-Forwarded-For` field name.
18pub const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
19
20/// The de facto, non-IETF `X-Forwarded-Proto` field name.
21pub const X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");
22
23/// Extract all `X-Forwarded-For` field lines as an untrusted asserted IP chain.
24///
25/// Addresses are returned in field order, from the remotest assertion to the
26/// one nearest the server. A missing field returns `None`. Repeated field lines
27/// are accepted in wire order, but a non-text line, empty comma-separated item,
28/// or invalid IP/socket-address item returns an error. Values are syntactically
29/// parsed only; no sender is authenticated and parsing does not make them
30/// trustworthy. A feature-gated [`crate::extract_client_ip`] convenience can
31/// select from this and other fields, but it does not authenticate the sender;
32/// establish trust out-of-band before using the result for a security decision.
33pub fn extract_header_x_forwarded_for(headers: &HeaderMap) -> Result<Option<Vec<IpAddr>>, Error> {
34    extract_comma_values(headers, &X_FORWARDED_FOR, |value| {
35        parse_ip(value, X_FORWARDED_FOR)
36    })
37}
38
39/// Extract the untrusted `X-Forwarded-For` chain from a complete request.
40///
41/// This reads `request.headers()` and delegates to
42/// [`extract_header_x_forwarded_for`], preserving its missing, repeated-line,
43/// and malformed-value behavior. The returned assertions remain untrusted.
44pub fn extract_request_x_forwarded_for<B>(
45    request: &Request<B>,
46) -> Result<Option<Vec<IpAddr>>, Error> {
47    extract_header_x_forwarded_for(request.headers())
48}
49
50/// Extract all `X-Forwarded-Proto` field lines as untrusted protocol tokens.
51///
52/// A missing field returns `None`. Repeated field lines are accepted in wire
53/// order. Each comma-separated item must be a non-empty URI-scheme token;
54/// non-text lines and invalid items return an error. Valid tokens are normalized
55/// to lowercase. This performs syntax validation only: it does not authenticate
56/// the sender, select an effective request scheme, or make the values trusted.
57pub fn extract_header_x_forwarded_proto(headers: &HeaderMap) -> Result<Option<Vec<String>>, Error> {
58    extract_comma_values(headers, &X_FORWARDED_PROTO, |value| {
59        if is_scheme(value) {
60            Ok(value.to_ascii_lowercase())
61        } else {
62            Err(Error::invalid_header(X_FORWARDED_PROTO))
63        }
64    })
65}
66
67/// Extract untrusted `X-Forwarded-Proto` tokens from a complete request.
68///
69/// This reads `request.headers()` and delegates to
70/// [`extract_header_x_forwarded_proto`], preserving its missing, repeated-line,
71/// normalization, and malformed-value behavior. The returned assertions remain
72/// untrusted.
73pub fn extract_request_x_forwarded_proto<B>(
74    request: &Request<B>,
75) -> Result<Option<Vec<String>>, Error> {
76    extract_header_x_forwarded_proto(request.headers())
77}
78
79/// Extract the rightmost `X-Forwarded-For` IP address from a header.
80///
81/// This reads `headers` and delegates to
82/// [`extract_header_x_forwarded_for`], preserving its missing and strict error
83/// behavior. It does not establish trust in the returned assertion.
84pub fn extract_rightmost_x_forwarded_for(headers: &HeaderMap) -> Result<Option<IpAddr>, Error> {
85    Ok(extract_header_x_forwarded_for(headers)?.and_then(|ips| ips.last().copied()))
86}
87
88/// Parse an IP address or socket address from a string.
89fn parse_ip(value: &str, name: HeaderName) -> Result<IpAddr, Error> {
90    if let Ok(address) = value.parse() {
91        return Ok(address);
92    }
93    if let Ok(address) = value.parse::<SocketAddr>() {
94        return Ok(address.ip());
95    }
96    Err(Error::invalid_header(name))
97}
98
99/// Extract comma-separated values from a header.
100fn extract_comma_values<T>(
101    headers: &HeaderMap,
102    name: &HeaderName,
103    mut parse: impl FnMut(&str) -> Result<T, Error>,
104) -> Result<Option<Vec<T>>, Error> {
105    let mut output = Vec::new();
106    let mut present = false;
107    for value in headers.get_all(name) {
108        present = true;
109        let value = value
110            .to_str()
111            .map_err(|_| Error::invalid_header(name.clone()))?;
112        for item in value.split(',') {
113            let item = item.trim();
114            if item.is_empty() {
115                return Err(Error::invalid_header(name.clone()));
116            }
117            output.push(parse(item)?);
118        }
119    }
120    Ok(present.then_some(output))
121}
122
123fn is_scheme(value: &str) -> bool {
124    let mut bytes = value.bytes();
125    matches!(bytes.next(), Some(byte) if byte.is_ascii_alphabetic())
126        && bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.'))
127}
128
129#[cfg(test)]
130mod tests {
131    use std::net::{IpAddr, Ipv4Addr};
132
133    use http::{HeaderMap, HeaderValue};
134
135    use super::*;
136
137    #[test]
138    fn extracts_x_forwarded_for_across_field_lines() {
139        let mut headers = HeaderMap::new();
140        headers.append(&X_FORWARDED_FOR, "192.0.2.1, 198.51.100.2".parse().unwrap());
141        headers.append(&X_FORWARDED_FOR, "203.0.113.3".parse().unwrap());
142
143        assert_eq!(
144            extract_header_x_forwarded_for(&headers).unwrap().unwrap(),
145            vec![
146                IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)),
147                IpAddr::V4(Ipv4Addr::new(198, 51, 100, 2)),
148                IpAddr::V4(Ipv4Addr::new(203, 0, 113, 3)),
149            ]
150        );
151    }
152
153    #[test]
154    fn rejects_invalid_x_forwarded_for_values() {
155        let mut headers = HeaderMap::new();
156        for value in [
157            "[2001:db8::1]",
158            "[2001:db8::1]junk",
159            "[2001:db8::1]:65536",
160            "[2001:db8::1]:99999",
161        ] {
162            headers.insert(&X_FORWARDED_FOR, value.parse().unwrap());
163            assert!(
164                matches!(
165                    extract_header_x_forwarded_for(&headers),
166                    Err(Error::InvalidHeader { .. })
167                ),
168                "unexpectedly accepted {value:?}",
169            );
170        }
171
172        headers.insert(&X_FORWARDED_FOR, HeaderValue::from_bytes(&[0xff]).unwrap());
173        assert!(matches!(
174            extract_header_x_forwarded_for(&headers),
175            Err(Error::InvalidHeader { .. })
176        ));
177    }
178
179    #[test]
180    fn accepts_bracketed_ipv6_with_valid_port() {
181        let mut headers = HeaderMap::new();
182        headers.insert(&X_FORWARDED_FOR, "[2001:db8::1]:65535".parse().unwrap());
183        assert_eq!(
184            extract_header_x_forwarded_for(&headers).unwrap(),
185            Some(vec!["2001:db8::1".parse().unwrap()]),
186        );
187    }
188
189    #[test]
190    fn extracts_and_normalizes_x_forwarded_proto() {
191        let mut headers = HeaderMap::new();
192        assert_eq!(extract_header_x_forwarded_proto(&headers).unwrap(), None);
193
194        headers.append(&X_FORWARDED_PROTO, "HTTPS, Web+TLS".parse().unwrap());
195        assert_eq!(
196            extract_header_x_forwarded_proto(&headers).unwrap().unwrap(),
197            vec!["https".to_owned(), "web+tls".to_owned()]
198        );
199
200        headers.insert(&X_FORWARDED_PROTO, "http_2".parse().unwrap());
201        assert!(matches!(
202            extract_header_x_forwarded_proto(&headers),
203            Err(Error::InvalidHeader { .. })
204        ));
205    }
206
207    #[test]
208    fn request_entry_points_delegate_to_headers() {
209        let request = Request::builder()
210            .header(&X_FORWARDED_FOR, "192.0.2.1")
211            .header(&X_FORWARDED_PROTO, "HTTPS")
212            .body(())
213            .unwrap();
214        assert_eq!(
215            extract_request_x_forwarded_for(&request).unwrap(),
216            Some(vec!["192.0.2.1".parse().unwrap()])
217        );
218        assert_eq!(
219            extract_request_x_forwarded_proto(&request).unwrap(),
220            Some(vec!["https".to_owned()])
221        );
222    }
223}