security_rust/protocol/
cors.rs1use regex::Regex;
4use std::sync::LazyLock;
5
6use crate::{AttackCategory, DetectionResult, Detector, Severity};
7
8static ACAO_NULL: LazyLock<Regex> =
17 LazyLock::new(|| Regex::new(r"(?i)Access-Control-Allow-Origin:\s*null\b").unwrap());
18static ACAO_WILDCARD: LazyLock<Regex> =
19 LazyLock::new(|| Regex::new(r"(?i)Access-Control-Allow-Origin:\s*\*").unwrap());
20static CREDS_TRUE: LazyLock<Regex> =
21 LazyLock::new(|| Regex::new(r"(?i)Access-Control-Allow-Credentials:\s*true").unwrap());
22static ORIGIN_NULL: LazyLock<Regex> =
23 LazyLock::new(|| Regex::new(r"(?i)Origin:\s*null\b").unwrap());
24
25pub struct CorsDetector;
26
27impl Detector for CorsDetector {
28 fn name(&self) -> &'static str {
29 "cors"
30 }
31
32 fn detect(&self, input: &str) -> Option<DetectionResult> {
33 let m = if let Some(m) = ACAO_NULL.find(input) {
36 m
37 } else if let Some(m) = ACAO_WILDCARD
38 .find(input)
39 .filter(|_| CREDS_TRUE.is_match(input))
40 {
41 m
42 } else {
43 ORIGIN_NULL.find(input)?
44 };
45 Some(DetectionResult {
46 attack_type: self.name().to_string(),
47 category: AttackCategory::Protocol,
48 severity: Severity::Medium,
49 matched_pattern: m.as_str().to_string(),
50 offset: m.start(),
51 message: "CORS bypass attempt detected".into(),
52 })
53 }
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 fn assert_detected(input: &str) {
61 crate::test_helpers::assert_detected(
62 &CorsDetector,
63 input,
64 AttackCategory::Protocol,
65 Severity::Medium,
66 );
67 }
68
69 fn assert_clean(input: &str) {
70 crate::test_helpers::assert_clean(&CorsDetector, input);
71 }
72
73 #[test]
74 fn name_is_cors() {
75 assert_eq!(CorsDetector.name(), "cors");
76 }
77
78 #[test]
79 fn detects_null_origin() {
80 assert_detected("Origin: null");
81 assert_detected("Origin:null");
82 }
83
84 #[test]
85 fn detects_null_allow_origin() {
86 assert_detected("Access-Control-Allow-Origin: null");
87 assert_detected("Access-Control-Allow-Origin:null");
88 let r = CorsDetector
90 .detect("Access-Control-Allow-Origin: null")
91 .unwrap();
92 assert_eq!(r.matched_pattern, "Access-Control-Allow-Origin: null");
93 }
94
95 #[test]
96 fn wildcard_alone_is_clean_but_with_credentials_is_not() {
97 assert_clean("Access-Control-Allow-Origin: *");
99 assert_clean("Access-Control-Allow-Origin:*");
100 assert_detected("Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Credentials: true");
102 assert_detected("Access-Control-Allow-Origin:*\nAccess-Control-Allow-Credentials:true");
103 }
104
105 #[test]
106 fn allow_credentials_alone_is_clean() {
107 assert_clean("Access-Control-Allow-Credentials: true");
109 }
110
111 #[test]
112 fn detects_mixed_case() {
113 assert_detected("origin: NULL");
114 assert_detected("access-control-allow-origin: null");
115 assert_detected("access-control-allow-origin: *\r\naccess-control-allow-credentials: TRUE");
116 }
117
118 #[test]
119 fn rejects_benign_headers() {
120 assert_clean("Origin: http://example.com");
121 assert_clean("Access-Control-Allow-Origin: https://example.com");
122 assert_clean("Access-Control-Allow-Origin: *");
123 assert_clean("Access-Control-Allow-Credentials: false");
124 assert_clean("Origin: *");
125 assert_clean(
127 "Origin: https://example.com\r\nAccess-Control-Allow-Origin: https://example.com",
128 );
129 assert_clean("Access-Control-Allow-Origin: nullify-me");
131 }
132
133 #[test]
134 fn rejects_space_before_colon() {
135 assert_clean("Access-Control-Allow-Origin : *");
136 }
137
138 #[test]
139 fn rejects_empty_and_whitespace() {
140 assert_clean("");
141 assert_clean(" ");
142 }
143
144 #[test]
145 fn rejects_unicode_text() {
146 assert_clean("跨域配置说明,无攻击");
147 }
148}