security_rust/protocol/
open_redirect.rs1use regex::Regex;
4use std::sync::LazyLock;
5
6use crate::{AttackCategory, DetectionResult, Detector, Severity};
7
8static PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
9 vec![
10 Regex::new(r"(?i)//[^/\s]+\.[a-z]{2,}").unwrap(),
11 Regex::new(r"(?i)javascript\s*:").unwrap(),
12 Regex::new(r"(?i)data\s*:\s*text/html").unwrap(),
13 Regex::new(r"(?i)data\s*:\s*text/plain").unwrap(),
14 ]
15});
16
17pub struct OpenRedirectDetector;
18
19impl Detector for OpenRedirectDetector {
20 fn name(&self) -> &'static str {
21 "open_redirect"
22 }
23
24 fn detect(&self, input: &str) -> Option<DetectionResult> {
25 for re in PATTERNS.iter() {
26 for m in re.find_iter(input) {
27 let start = m.start();
28 if start > 0 && input.as_bytes()[start - 1] == b':' {
30 continue;
31 }
32 return Some(DetectionResult {
33 attack_type: "open_redirect".into(),
34 category: AttackCategory::Protocol,
35 severity: Severity::Medium,
36 matched_pattern: m.as_str().to_string(),
37 offset: start,
38 message: "Open redirect detected".into(),
39 });
40 }
41 }
42 None
43 }
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49
50 fn assert_detected(input: &str) {
51 crate::test_helpers::assert_detected(
52 &OpenRedirectDetector,
53 input,
54 AttackCategory::Protocol,
55 Severity::Medium,
56 );
57 }
58
59 fn assert_clean(input: &str) {
60 crate::test_helpers::assert_clean(&OpenRedirectDetector, input);
61 }
62
63 #[test]
64 fn name_is_open_redirect() {
65 assert_eq!(OpenRedirectDetector.name(), "open_redirect");
66 }
67
68 #[test]
69 fn detects_double_slash_url() {
70 assert_detected("//evil.com/phishing");
71 assert_detected("//attacker.org/steal?u=1");
72 assert_detected("https://ok.com//evil.com"); }
74
75 #[test]
76 fn detects_javascript_uri() {
77 assert_detected("javascript:alert(document.cookie)");
78 }
79
80 #[test]
81 fn detects_data_html_uri() {
82 assert_detected("data:text/html,<script>alert(1)</script>");
83 }
84
85 #[test]
86 fn detects_data_plain_uri() {
87 assert_detected("data: text/plain;base64,SGVsbG8=");
88 }
89
90 #[test]
91 fn detects_mixed_case() {
92 assert_detected("JAVASCRIPT:alert(1)");
93 assert_detected("//Evil.Com/");
94 }
95
96 #[test]
97 fn rejects_scheme_urls() {
98 assert_clean("https://evil.com/phishing");
99 assert_clean("http://example.com/");
100 assert_clean("file:///etc/passwd");
101 }
102
103 #[test]
104 fn rejects_benign_paths() {
105 assert_clean("example.com/redirect");
106 assert_clean("java script: alert(1)");
107 assert_clean("data:image/png;base64,AA==");
108 }
109
110 #[test]
111 fn rejects_near_misses() {
112 assert_clean("//evil/com");
113 assert_clean("//evil.c");
114 assert_clean("// evil.com");
115 }
116
117 #[test]
118 fn rejects_empty_and_whitespace() {
119 assert_clean("");
120 assert_clean(" ");
121 }
122
123 #[test]
124 fn rejects_unicode_text() {
125 assert_clean("重定向到登录页");
126 }
127}