Skip to main content

fraiseql_auth/
proxy.rs

1//! Proxy and IP address extraction with security validation
2
3use std::net::IpAddr;
4
5/// Validate that a string is a valid IP address format
6///
7/// # SECURITY
8/// Prevents injection attacks where malformed IPs could bypass validation.
9/// Returns None for any invalid IP format.
10fn validate_ip_format(ip_str: &str) -> Option<IpAddr> {
11    ip_str.parse::<IpAddr>().ok()
12}
13
14/// Proxy configuration for X-Forwarded-For header validation
15#[derive(Debug, Clone)]
16pub struct ProxyConfig {
17    /// List of trusted proxy IPs (e.g., load balancer, Nginx, HAProxy IPs)
18    /// Only X-Forwarded-For headers from these IPs are trusted
19    pub trusted_proxies:       Vec<IpAddr>,
20    /// If true, require request to come from a trusted proxy to use X-Forwarded-For
21    pub require_trusted_proxy: bool,
22}
23
24impl ProxyConfig {
25    /// Create a new proxy configuration
26    #[must_use]
27    pub const fn new(trusted_proxies: Vec<IpAddr>, require_trusted_proxy: bool) -> Self {
28        Self {
29            trusted_proxies,
30            require_trusted_proxy,
31        }
32    }
33
34    /// Create a proxy config that trusts all local proxies (127.0.0.1 only).
35    ///
36    /// # Panics
37    ///
38    /// Cannot panic — the IP literal `"127.0.0.1"` is always valid.
39    #[must_use]
40    pub fn localhost_only() -> Self {
41        Self {
42            trusted_proxies:       vec!["127.0.0.1".parse().expect("valid IP")], /* Reason: "127.0.0.1" is a compile-time literal and always parses successfully */
43            require_trusted_proxy: true,
44        }
45    }
46
47    /// Create a proxy config with no trusted proxies
48    #[must_use]
49    pub const fn none() -> Self {
50        Self {
51            trusted_proxies:       Vec::new(),
52            require_trusted_proxy: false,
53        }
54    }
55
56    /// Check if an IP address is a trusted proxy
57    ///
58    /// # SECURITY
59    /// Validates IP format before checking against trusted list.
60    /// Returns false for any invalid IP format, preventing bypass attempts.
61    #[must_use]
62    pub fn is_trusted_proxy(&self, ip: &str) -> bool {
63        if self.trusted_proxies.is_empty() {
64            return false;
65        }
66
67        // Validate IP format and check against trusted list
68        match validate_ip_format(ip) {
69            Some(addr) => self.trusted_proxies.contains(&addr),
70            None => false, // Invalid IP format is not trusted
71        }
72    }
73
74    /// Extract client IP from headers with security validation
75    ///
76    /// # SECURITY
77    /// Only trusts X-Forwarded-For if the request comes from a trusted proxy.
78    /// Falls back to direct connection IP if X-Forwarded-For cannot be validated.
79    /// Validates all extracted IPs to ensure proper format.
80    ///
81    /// This prevents IP spoofing attacks where an attacker sends a malicious
82    /// X-Forwarded-For header to bypass rate limiting or access controls.
83    #[must_use]
84    pub fn extract_client_ip(
85        &self,
86        headers: &axum::http::HeaderMap,
87        socket_addr: Option<std::net::SocketAddr>,
88    ) -> Option<String> {
89        let direct_ip = socket_addr.map(|addr| addr.ip().to_string());
90
91        // If no direct IP available, return early
92        let direct_ip_str = direct_ip.as_deref().unwrap_or("");
93
94        // Check X-Forwarded-For if proxy is trusted
95        if let Some(forwarded_for) = headers.get("x-forwarded-for").and_then(|v| v.to_str().ok()) {
96            if self.is_trusted_proxy(direct_ip_str) {
97                // Extract first IP from X-Forwarded-For (client IP in chain)
98                if let Some(ip_str) = forwarded_for.split(',').next().map(|ip| ip.trim()) {
99                    // SECURITY: Validate IP format before returning
100                    if validate_ip_format(ip_str).is_some() {
101                        return Some(ip_str.to_string());
102                    }
103                    // Invalid IP format - fall through to use direct IP
104                }
105            }
106            // X-Forwarded-For present but from untrusted proxy - ignore it and use direct IP
107            if let Some(ip) = direct_ip {
108                return Some(ip);
109            }
110        }
111
112        // Check X-Real-IP if proxy is trusted
113        if let Some(real_ip) = headers.get("x-real-ip").and_then(|v| v.to_str().ok()) {
114            if self.is_trusted_proxy(direct_ip_str) {
115                // SECURITY: Validate IP format before returning
116                if validate_ip_format(real_ip).is_some() {
117                    return Some(real_ip.to_string());
118                }
119                // Invalid IP format - fall through to use direct IP
120            }
121            // X-Real-IP present but from untrusted proxy - ignore it and use direct IP
122            if let Some(ip) = direct_ip {
123                return Some(ip);
124            }
125        }
126
127        // Fall back to direct connection IP (already validated by Axum)
128        direct_ip
129    }
130}