use regex::Regex;
use std::sync::LazyLock;
use crate::{AttackCategory, DetectionResult, Detector, Severity};
static ACAO_NULL: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)Access-Control-Allow-Origin:\s*null\b").unwrap());
static ACAO_WILDCARD: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)Access-Control-Allow-Origin:\s*\*").unwrap());
static CREDS_TRUE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)Access-Control-Allow-Credentials:\s*true").unwrap());
static ORIGIN_NULL: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)Origin:\s*null\b").unwrap());
pub struct CorsDetector;
impl Detector for CorsDetector {
fn name(&self) -> &'static str {
"cors"
}
fn detect(&self, input: &str) -> Option<DetectionResult> {
let m = if let Some(m) = ACAO_NULL.find(input) {
m
} else if let Some(m) = ACAO_WILDCARD
.find(input)
.filter(|_| CREDS_TRUE.is_match(input))
{
m
} else {
ORIGIN_NULL.find(input)?
};
Some(DetectionResult {
attack_type: self.name().to_string(),
category: AttackCategory::Protocol,
severity: Severity::Medium,
matched_pattern: m.as_str().to_string(),
offset: m.start(),
message: "CORS bypass attempt detected".into(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_detected(input: &str) {
crate::test_helpers::assert_detected(
&CorsDetector,
input,
AttackCategory::Protocol,
Severity::Medium,
);
}
fn assert_clean(input: &str) {
crate::test_helpers::assert_clean(&CorsDetector, input);
}
#[test]
fn name_is_cors() {
assert_eq!(CorsDetector.name(), "cors");
}
#[test]
fn detects_null_origin() {
assert_detected("Origin: null");
assert_detected("Origin:null");
}
#[test]
fn detects_null_allow_origin() {
assert_detected("Access-Control-Allow-Origin: null");
assert_detected("Access-Control-Allow-Origin:null");
let r = CorsDetector
.detect("Access-Control-Allow-Origin: null")
.unwrap();
assert_eq!(r.matched_pattern, "Access-Control-Allow-Origin: null");
}
#[test]
fn wildcard_alone_is_clean_but_with_credentials_is_not() {
assert_clean("Access-Control-Allow-Origin: *");
assert_clean("Access-Control-Allow-Origin:*");
assert_detected("Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Credentials: true");
assert_detected("Access-Control-Allow-Origin:*\nAccess-Control-Allow-Credentials:true");
}
#[test]
fn allow_credentials_alone_is_clean() {
assert_clean("Access-Control-Allow-Credentials: true");
}
#[test]
fn detects_mixed_case() {
assert_detected("origin: NULL");
assert_detected("access-control-allow-origin: null");
assert_detected("access-control-allow-origin: *\r\naccess-control-allow-credentials: TRUE");
}
#[test]
fn rejects_benign_headers() {
assert_clean("Origin: http://example.com");
assert_clean("Access-Control-Allow-Origin: https://example.com");
assert_clean("Access-Control-Allow-Origin: *");
assert_clean("Access-Control-Allow-Credentials: false");
assert_clean("Origin: *");
assert_clean(
"Origin: https://example.com\r\nAccess-Control-Allow-Origin: https://example.com",
);
assert_clean("Access-Control-Allow-Origin: nullify-me");
}
#[test]
fn rejects_space_before_colon() {
assert_clean("Access-Control-Allow-Origin : *");
}
#[test]
fn rejects_empty_and_whitespace() {
assert_clean("");
assert_clean(" ");
}
#[test]
fn rejects_unicode_text() {
assert_clean("跨域配置说明,无攻击");
}
}