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 `client_ip::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
88fn parse_ip(value: &str, name: HeaderName) -> Result<IpAddr, Error> {
89    if let Ok(address) = value.parse() {
90        return Ok(address);
91    }
92    if let Ok(address) = value.parse::<SocketAddr>() {
93        return Ok(address.ip());
94    }
95    if let Some(rest) = value.strip_prefix('[')
96        && let Some((inner, suffix)) = rest.split_once(']')
97        && (suffix.is_empty()
98            || suffix.strip_prefix(':').is_some_and(|port| {
99                !port.is_empty() && port.bytes().all(|byte| byte.is_ascii_digit())
100            }))
101        && let Ok(address) = inner.parse()
102    {
103        return Ok(address);
104    }
105    Err(Error::invalid_header(name))
106}
107
108fn extract_comma_values<T>(
109    headers: &HeaderMap,
110    name: &HeaderName,
111    mut parse: impl FnMut(&str) -> Result<T, Error>,
112) -> Result<Option<Vec<T>>, Error> {
113    let mut output = Vec::new();
114    let mut present = false;
115    for value in headers.get_all(name) {
116        present = true;
117        let value = value
118            .to_str()
119            .map_err(|_| Error::invalid_header(name.clone()))?;
120        for item in value.split(',') {
121            let item = item.trim();
122            if item.is_empty() {
123                return Err(Error::invalid_header(name.clone()));
124            }
125            output.push(parse(item)?);
126        }
127    }
128    Ok(present.then_some(output))
129}
130
131fn is_scheme(value: &str) -> bool {
132    let mut bytes = value.bytes();
133    matches!(bytes.next(), Some(byte) if byte.is_ascii_alphabetic())
134        && bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.'))
135}
136
137#[cfg(test)]
138mod tests {
139    use std::net::{IpAddr, Ipv4Addr};
140
141    use http::{HeaderMap, HeaderValue};
142
143    use super::*;
144
145    #[test]
146    fn extracts_x_forwarded_for_across_field_lines() {
147        let mut headers = HeaderMap::new();
148        headers.append(&X_FORWARDED_FOR, "192.0.2.1, 198.51.100.2".parse().unwrap());
149        headers.append(&X_FORWARDED_FOR, "203.0.113.3".parse().unwrap());
150
151        assert_eq!(
152            extract_header_x_forwarded_for(&headers).unwrap().unwrap(),
153            vec![
154                IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)),
155                IpAddr::V4(Ipv4Addr::new(198, 51, 100, 2)),
156                IpAddr::V4(Ipv4Addr::new(203, 0, 113, 3)),
157            ]
158        );
159    }
160
161    #[test]
162    fn rejects_invalid_x_forwarded_for_values() {
163        let mut headers = HeaderMap::new();
164        headers.insert(&X_FORWARDED_FOR, "[2001:db8::1]junk".parse().unwrap());
165        assert!(matches!(
166            extract_header_x_forwarded_for(&headers),
167            Err(Error::InvalidHeader { .. })
168        ));
169
170        headers.insert(&X_FORWARDED_FOR, HeaderValue::from_bytes(&[0xff]).unwrap());
171        assert!(matches!(
172            extract_header_x_forwarded_for(&headers),
173            Err(Error::InvalidHeader { .. })
174        ));
175    }
176
177    #[test]
178    fn extracts_and_normalizes_x_forwarded_proto() {
179        let mut headers = HeaderMap::new();
180        assert_eq!(extract_header_x_forwarded_proto(&headers).unwrap(), None);
181
182        headers.append(&X_FORWARDED_PROTO, "HTTPS, Web+TLS".parse().unwrap());
183        assert_eq!(
184            extract_header_x_forwarded_proto(&headers).unwrap().unwrap(),
185            vec!["https".to_owned(), "web+tls".to_owned()]
186        );
187
188        headers.insert(&X_FORWARDED_PROTO, "http_2".parse().unwrap());
189        assert!(matches!(
190            extract_header_x_forwarded_proto(&headers),
191            Err(Error::InvalidHeader { .. })
192        ));
193    }
194
195    #[test]
196    fn request_entry_points_delegate_to_headers() {
197        let request = Request::builder()
198            .header(&X_FORWARDED_FOR, "192.0.2.1")
199            .header(&X_FORWARDED_PROTO, "HTTPS")
200            .body(())
201            .unwrap();
202        assert_eq!(
203            extract_request_x_forwarded_for(&request).unwrap(),
204            Some(vec!["192.0.2.1".parse().unwrap()])
205        );
206        assert_eq!(
207            extract_request_x_forwarded_proto(&request).unwrap(),
208            Some(vec!["https".to_owned()])
209        );
210    }
211}