Skip to main content

security_rust/protocol/
ssrf.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3use regex::Regex;
4use std::sync::LazyLock;
5
6use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
7
8static PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
9    vec![
10        Regex::new(r"(?i)169\.254\.169\.254").unwrap(),
11        Regex::new(r"(?i)10\.\d{1,3}\.\d{1,3}\.\d{1,3}").unwrap(),
12        Regex::new(r"(?i)172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}").unwrap(),
13        Regex::new(r"(?i)192\.168\.\d{1,3}\.\d{1,3}").unwrap(),
14        Regex::new(r"(?i)127\.\d{1,3}\.\d{1,3}\.\d{1,3}").unwrap(),
15        Regex::new(r"(?i)\[::1\]").unwrap(),
16        Regex::new(r"(?i)0\.0\.0\.0").unwrap(),
17        Regex::new(r"(?i)gopher://").unwrap(),
18        Regex::new(r"(?i)dict://").unwrap(),
19        Regex::new(r"(?i)ftp://[^/]*@").unwrap(),
20        Regex::new(r"(?i)file:///").unwrap(),
21    ]
22});
23
24pub struct SsrfDetector;
25
26impl Detector for SsrfDetector {
27    fn name(&self) -> &'static str {
28        "ssrf"
29    }
30
31    fn detect(&self, input: &str) -> Option<DetectionResult> {
32        regex_detect(
33            &PATTERNS,
34            self.name(),
35            AttackCategory::Protocol,
36            Severity::Critical,
37            "SSRF server-side request forgery detected",
38            input,
39        )
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46
47    fn assert_detected(input: &str) {
48        crate::test_helpers::assert_detected(
49            &SsrfDetector,
50            input,
51            AttackCategory::Protocol,
52            Severity::Critical,
53        );
54    }
55
56    fn assert_clean(input: &str) {
57        crate::test_helpers::assert_clean(&SsrfDetector, input);
58    }
59
60    #[test]
61    fn name_is_ssrf() {
62        assert_eq!(SsrfDetector.name(), "ssrf");
63    }
64
65    #[test]
66    fn detects_cloud_metadata_ip() {
67        assert_detected("http://169.254.169.254/latest/meta-data/");
68    }
69
70    #[test]
71    fn detects_internal_ipv4() {
72        assert_detected("http://10.0.0.1/admin");
73    }
74
75    #[test]
76    fn detects_private_ranges() {
77        assert_detected("http://192.168.1.1/");
78        assert_detected("http://172.16.0.1/");
79        assert_detected("http://172.31.255.255/");
80        assert_detected("http://127.0.0.1:8080/");
81        assert_detected("http://0.0.0.0/");
82        assert_detected("http://[::1]/");
83    }
84
85    #[test]
86    fn detects_ssrf_uri_schemes() {
87        assert_detected("gopher://evil.com/_GET / HTTP/1.1");
88        assert_detected("dict://evil.com:11211/");
89        assert_detected("ftp://user@evil.com/file");
90        assert_detected("file:///etc/passwd");
91    }
92
93    #[test]
94    fn detects_mixed_case_schemes() {
95        assert_detected("GOPHER://evil.com/");
96        assert_detected("FILE:///etc/shadow");
97    }
98
99    #[test]
100    fn rejects_public_hosts() {
101        assert_clean("http://example.com/index.html");
102        assert_clean("https://github.com/security-rust");
103        assert_clean("http://172.32.0.1/");
104        assert_clean("http://8.8.8.8/dns");
105    }
106
107    #[test]
108    fn rejects_near_misses() {
109        assert_clean("http://10.0.0/admin");
110        assert_clean("http://169.254.169/admin");
111        assert_clean("ftp://evil.com/pub");
112        assert_clean("http://192.168/admin");
113    }
114
115    #[test]
116    fn rejects_empty_and_whitespace() {
117        assert_clean("");
118        assert_clean("   ");
119        assert_clean("\t\n");
120    }
121
122    #[test]
123    fn rejects_unicode_text() {
124        assert_clean("你好,世界!这是一个正常的中文文本。");
125    }
126}