Skip to main content

feedparser_rs/http/
validation.rs

1use crate::error::Result;
2use crate::util::ssrf;
3use url::Url;
4
5/// Validates a URL to prevent Server-Side Request Forgery (SSRF) attacks
6///
7/// This function ensures that URLs only point to public, safe destinations.
8/// It is the single validation entry point used both before sending the
9/// initial request and to re-validate every redirect hop
10/// (see [`crate::http::FeedHttpClient`]), and shares its rule set with
11/// `xml:base` resolution via [`crate::util::base_url::is_safe_url`].
12///
13/// # Security Checks
14///
15/// 1. Only HTTP and HTTPS schemes are allowed
16/// 2. Private IP ranges are blocked (RFC 1918, RFC 4193)
17/// 3. Localhost and loopback addresses are blocked
18/// 4. Link-local addresses are blocked (169.254.0.0/16)
19/// 5. Cloud metadata endpoints are blocked
20/// 6. Internal domain names are blocked (.local, .internal)
21///
22/// # Errors
23///
24/// Returns `FeedError::Http` if:
25/// - The URL is malformed or invalid
26/// - The URL scheme is not HTTP or HTTPS
27/// - The URL points to a private IP address, localhost, or internal domain
28/// - The URL points to a cloud metadata endpoint
29///
30/// # Examples
31///
32/// ```
33/// use feedparser_rs::http::validation::validate_url;
34///
35/// // These are allowed
36/// assert!(validate_url("https://example.com/feed.xml").is_ok());
37/// assert!(validate_url("http://blog.example.org/rss").is_ok());
38///
39/// // These are blocked
40/// assert!(validate_url("http://localhost/").is_err());
41/// assert!(validate_url("http://192.168.1.1/").is_err());
42/// assert!(validate_url("http://169.254.169.254/").is_err());
43/// assert!(validate_url("file:///etc/passwd").is_err());
44/// ```
45pub fn validate_url(url_str: &str) -> Result<Url> {
46    ssrf::validate_url(url_str)
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    // Positive tests - these should pass
54    #[test]
55    fn test_valid_http_url() {
56        assert!(validate_url("http://example.com/feed.xml").is_ok());
57    }
58
59    #[test]
60    fn test_valid_https_url() {
61        assert!(validate_url("https://blog.example.org/rss").is_ok());
62    }
63
64    #[test]
65    fn test_valid_with_port() {
66        assert!(validate_url("https://example.com:8443/feed").is_ok());
67    }
68
69    #[test]
70    fn test_valid_with_path() {
71        assert!(validate_url("https://example.com/path/to/feed.xml").is_ok());
72    }
73
74    // Negative tests - scheme validation
75    #[test]
76    fn test_reject_file_scheme() {
77        assert!(validate_url("file:///etc/passwd").is_err());
78    }
79
80    #[test]
81    fn test_reject_ftp_scheme() {
82        assert!(validate_url("ftp://example.com/file").is_err());
83    }
84
85    #[test]
86    fn test_reject_javascript_scheme() {
87        assert!(validate_url("javascript:alert(1)").is_err());
88    }
89
90    #[test]
91    fn test_reject_data_scheme() {
92        assert!(validate_url("data:text/html,<script>alert(1)</script>").is_err());
93    }
94
95    // Negative tests - IPv4 private ranges
96    #[test]
97    fn test_reject_ipv4_private_10() {
98        assert!(validate_url("http://10.0.0.1/").is_err());
99        assert!(validate_url("http://10.255.255.255/").is_err());
100    }
101
102    #[test]
103    fn test_reject_ipv4_private_172() {
104        assert!(validate_url("http://172.16.0.1/").is_err());
105        assert!(validate_url("http://172.31.255.255/").is_err());
106    }
107
108    #[test]
109    fn test_reject_ipv4_private_192() {
110        assert!(validate_url("http://192.168.0.1/").is_err());
111        assert!(validate_url("http://192.168.255.255/").is_err());
112    }
113
114    #[test]
115    fn test_reject_ipv4_localhost() {
116        assert!(validate_url("http://127.0.0.1/").is_err());
117        assert!(validate_url("http://127.0.0.2/").is_err());
118    }
119
120    #[test]
121    fn test_reject_ipv4_link_local() {
122        assert!(validate_url("http://169.254.169.254/").is_err());
123        assert!(validate_url("http://169.254.0.1/").is_err());
124    }
125
126    #[test]
127    fn test_reject_ipv4_zero() {
128        assert!(validate_url("http://0.0.0.0/").is_err());
129    }
130
131    #[test]
132    fn test_reject_ipv4_broadcast() {
133        assert!(validate_url("http://255.255.255.255/").is_err());
134    }
135
136    // Negative tests - IPv6
137    #[test]
138    fn test_reject_ipv6_loopback() {
139        assert!(validate_url("http://[::1]/").is_err());
140    }
141
142    #[test]
143    fn test_reject_ipv6_link_local() {
144        assert!(validate_url("http://[fe80::1]/").is_err());
145    }
146
147    #[test]
148    fn test_reject_ipv6_unique_local() {
149        assert!(validate_url("http://[fc00::1]/").is_err());
150        assert!(validate_url("http://[fd00::1]/").is_err());
151    }
152
153    // Negative tests - domain names
154    #[test]
155    fn test_reject_localhost_domain() {
156        assert!(validate_url("http://localhost/").is_err());
157    }
158
159    #[test]
160    fn test_reject_local_tld() {
161        assert!(validate_url("http://myserver.local/").is_err());
162    }
163
164    #[test]
165    fn test_reject_internal_tld() {
166        assert!(validate_url("http://server.internal/").is_err());
167    }
168
169    #[test]
170    fn test_reject_cloud_metadata() {
171        assert!(validate_url("http://metadata.google.internal/").is_err());
172        assert!(validate_url("http://metadata.azure.com/").is_err());
173    }
174
175    // Edge cases
176    #[test]
177    fn test_reject_no_host() {
178        assert!(validate_url("http://").is_err());
179    }
180
181    #[test]
182    fn test_reject_invalid_url() {
183        assert!(validate_url("not a url").is_err());
184    }
185
186    #[test]
187    fn test_public_ip_allowed() {
188        // Public IPs should be allowed
189        assert!(validate_url("http://8.8.8.8/").is_ok());
190        assert!(validate_url("http://1.1.1.1/").is_ok());
191    }
192
193    #[test]
194    fn test_carrier_grade_nat_blocked() {
195        assert!(validate_url("http://100.64.0.1/").is_err());
196        assert!(validate_url("http://100.127.255.255/").is_err());
197    }
198
199    #[test]
200    fn test_ipv6_multicast_blocked() {
201        assert!(validate_url("http://[ff00::1]/").is_err());
202        assert!(validate_url("http://[ff02::1]/").is_err());
203    }
204}