Skip to main content

max / makenotwork

6.0 KB · 164 lines History Blame Raw
1 //! Rate-limit key extraction that never trusts an attacker-controlled
2 //! `X-Forwarded-For`.
3 //!
4 //! `tower_governor`'s `SmartIpKeyExtractor` reads `X-Forwarded-For` from *every*
5 //! request, so any direct client can mint a fresh rate-limit bucket per request
6 //! by rotating the header, defeating every limiter. [`TrustedProxyKeyExtractor`]
7 //! closes that: it honors `X-Forwarded-For` only when the request's *direct
8 //! peer* is a configured trusted proxy, and otherwise keys on the peer address
9 //! and ignores the header entirely. There is deliberately **no `Default`**, the
10 //! set of trusted proxies must be supplied explicitly, so "trust this header"
11 //! can never be the silent default that the prior extractor made it.
12
13 use std::net::{IpAddr, SocketAddr};
14 use std::sync::Arc;
15
16 use axum::http::{HeaderMap, Request};
17 use tower_governor::errors::GovernorError;
18 use tower_governor::key_extractor::KeyExtractor;
19
20 /// A [`KeyExtractor`] that keys on the real client IP, trusting
21 /// `X-Forwarded-For` only when the direct peer is a known proxy.
22 ///
23 /// Construction requires the trusted-proxy set; there is no `Default`. An empty
24 /// set trusts no proxy, every request keys on its direct peer, which is safe
25 /// but collapses to a single global bucket behind a reverse proxy, so a proxied
26 /// deployment must list its proxy (loopback for an on-host Caddy).
27 #[derive(Clone, Debug)]
28 pub struct TrustedProxyKeyExtractor {
29 trusted_proxies: Arc<[IpAddr]>,
30 }
31
32 impl TrustedProxyKeyExtractor {
33 /// Build with the explicit set of proxy IPs whose `X-Forwarded-For` is
34 /// trusted. Trusting the header is a decision the caller must make here.
35 pub fn new(trusted_proxies: impl Into<Arc<[IpAddr]>>) -> Self {
36 Self {
37 trusted_proxies: trusted_proxies.into(),
38 }
39 }
40
41 fn peer_ip<T>(req: &Request<T>) -> Option<IpAddr> {
42 req.extensions()
43 .get::<axum::extract::ConnectInfo<SocketAddr>>()
44 .map(|ci| ci.0.ip())
45 }
46 }
47
48 impl KeyExtractor for TrustedProxyKeyExtractor {
49 type Key = IpAddr;
50
51 // `name`/`key_name` are only trait methods under tower_governor's `tracing`
52 // feature, which this crate does not enable; omitted deliberately.
53
54 fn extract<T>(&self, req: &Request<T>) -> Result<Self::Key, GovernorError> {
55 let peer = Self::peer_ip(req).ok_or(GovernorError::UnableToExtractKey)?;
56 // Only a trusted proxy's X-Forwarded-For is believed. A direct (untrusted)
57 // peer keys on its own socket address, so it cannot spoof another key.
58 if self.trusted_proxies.contains(&peer)
59 && let Some(client) = rightmost_xff(req.headers())
60 {
61 return Ok(client);
62 }
63 Ok(peer)
64 }
65 }
66
67 /// The rightmost parseable `X-Forwarded-For` entry, the address the trusted
68 /// proxy observed connecting to it. Taking the *rightmost* entry (not the
69 /// leftmost) means a client-injected hop to the left of the proxy's appended
70 /// entry is ignored, so spoofing fails even when the proxy appends rather than
71 /// overwrites.
72 fn rightmost_xff(headers: &HeaderMap) -> Option<IpAddr> {
73 headers
74 .get("x-forwarded-for")
75 .and_then(|hv| hv.to_str().ok())
76 .and_then(|s| s.rsplit(',').find_map(|p| p.trim().parse::<IpAddr>().ok()))
77 }
78
79 #[cfg(test)]
80 mod tests {
81 use super::*;
82
83 fn req(peer: Option<&str>, xff: Option<&str>) -> Request<()> {
84 let mut b = Request::builder();
85 if let Some(xff) = xff {
86 b = b.header("x-forwarded-for", xff);
87 }
88 let mut req = b.body(()).unwrap();
89 if let Some(peer) = peer {
90 let addr: SocketAddr = format!("{peer}:1234").parse().unwrap();
91 req.extensions_mut()
92 .insert(axum::extract::ConnectInfo(addr));
93 }
94 req
95 }
96
97 fn extractor(trusted: &[&str]) -> TrustedProxyKeyExtractor {
98 let ips: Vec<IpAddr> = trusted.iter().map(|s| s.parse().unwrap()).collect();
99 TrustedProxyKeyExtractor::new(ips)
100 }
101
102 #[test]
103 fn trusted_peer_uses_forwarded_client() {
104 // Peer is the trusted proxy → believe XFF (rightmost = real client).
105 let key = extractor(&["127.0.0.1"])
106 .extract(&req(Some("127.0.0.1"), Some("203.0.113.7")))
107 .unwrap();
108 assert_eq!(key, "203.0.113.7".parse::<IpAddr>().unwrap());
109 }
110
111 #[test]
112 fn untrusted_peer_ignores_forwarded_header() {
113 // A direct client spoofing XFF gets keyed on its OWN address, not the
114 // forged one, the whole point of the fix.
115 let key = extractor(&["127.0.0.1"])
116 .extract(&req(Some("198.51.100.9"), Some("203.0.113.7")))
117 .unwrap();
118 assert_eq!(key, "198.51.100.9".parse::<IpAddr>().unwrap());
119 }
120
121 #[test]
122 fn trusted_peer_no_header_falls_back_to_peer() {
123 let key = extractor(&["127.0.0.1"])
124 .extract(&req(Some("127.0.0.1"), None))
125 .unwrap();
126 assert_eq!(key, "127.0.0.1".parse::<IpAddr>().unwrap());
127 }
128
129 #[test]
130 fn rightmost_entry_defeats_injected_left_hop() {
131 // Attacker sends "1.2.3.4", proxy appends the real client on the right.
132 // Rightmost wins, so the injected left entry is ignored.
133 let key = extractor(&["127.0.0.1"])
134 .extract(&req(Some("127.0.0.1"), Some("1.2.3.4, 203.0.113.7")))
135 .unwrap();
136 assert_eq!(key, "203.0.113.7".parse::<IpAddr>().unwrap());
137 }
138
139 #[test]
140 fn trusted_peer_garbage_header_falls_back_to_peer() {
141 let key = extractor(&["127.0.0.1"])
142 .extract(&req(Some("127.0.0.1"), Some("not-an-ip")))
143 .unwrap();
144 assert_eq!(key, "127.0.0.1".parse::<IpAddr>().unwrap());
145 }
146
147 #[test]
148 fn empty_trusted_set_always_keys_on_peer() {
149 let key = extractor(&[])
150 .extract(&req(Some("203.0.113.7"), Some("10.0.0.1")))
151 .unwrap();
152 assert_eq!(key, "203.0.113.7".parse::<IpAddr>().unwrap());
153 }
154
155 #[test]
156 fn no_peer_errors() {
157 assert!(
158 extractor(&["127.0.0.1"])
159 .extract(&req(None, Some("203.0.113.7")))
160 .is_err()
161 );
162 }
163 }
164