security_rust/protocol/
ssrf.rs1use regex::Regex;
4use std::sync::LazyLock;
5
6use crate::{regex_detect, AttackCategory, DetectionResult, Detector, Severity};
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(&PATTERNS, self.name(), AttackCategory::Protocol, Severity::Critical, "SSRF server-side request forgery detected", input)
33 }
34}
35
36#[cfg(test)]
37mod tests {
38 use super::*;
39
40 fn assert_detected(input: &str) {
41 crate::test_helpers::assert_detected(
42 &SsrfDetector,
43 input,
44 AttackCategory::Protocol,
45 Severity::Critical,
46 );
47 }
48
49 fn assert_clean(input: &str) {
50 crate::test_helpers::assert_clean(&SsrfDetector, input);
51 }
52
53 #[test]
54 fn name_is_ssrf() {
55 assert_eq!(SsrfDetector.name(), "ssrf");
56 }
57
58 #[test]
59 fn detects_cloud_metadata_ip() {
60 assert_detected("http://169.254.169.254/latest/meta-data/");
61 }
62
63 #[test]
64 fn detects_internal_ipv4() {
65 assert_detected("http://10.0.0.1/admin");
66 }
67
68 #[test]
69 fn detects_private_ranges() {
70 assert_detected("http://192.168.1.1/");
71 assert_detected("http://172.16.0.1/");
72 assert_detected("http://172.31.255.255/");
73 assert_detected("http://127.0.0.1:8080/");
74 assert_detected("http://0.0.0.0/");
75 assert_detected("http://[::1]/");
76 }
77
78 #[test]
79 fn detects_ssrf_uri_schemes() {
80 assert_detected("gopher://evil.com/_GET / HTTP/1.1");
81 assert_detected("dict://evil.com:11211/");
82 assert_detected("ftp://user@evil.com/file");
83 assert_detected("file:///etc/passwd");
84 }
85
86 #[test]
87 fn detects_mixed_case_schemes() {
88 assert_detected("GOPHER://evil.com/");
89 assert_detected("FILE:///etc/shadow");
90 }
91
92 #[test]
93 fn rejects_public_hosts() {
94 assert_clean("http://example.com/index.html");
95 assert_clean("https://github.com/security-rust");
96 assert_clean("http://172.32.0.1/");
97 assert_clean("http://8.8.8.8/dns");
98 }
99
100 #[test]
101 fn rejects_near_misses() {
102 assert_clean("http://10.0.0/admin");
103 assert_clean("http://169.254.169/admin");
104 assert_clean("ftp://evil.com/pub");
105 assert_clean("http://192.168/admin");
106 }
107
108 #[test]
109 fn rejects_empty_and_whitespace() {
110 assert_clean("");
111 assert_clean(" ");
112 assert_clean("\t\n");
113 }
114
115 #[test]
116 fn rejects_unicode_text() {
117 assert_clean("你好,世界!这是一个正常的中文文本。");
118 }
119}