use regex::Regex;
use std::sync::LazyLock;
use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
static WEAK_PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"(?i)Host:\s*127\.|Host:\s*10\.|Host:\s*192\.168\.|Host:\s*172\.(1[6-9]|2\d|3[01])|Host:\s*localhost|Host:\s*\[::1\]|Host:\s*0\.0\.0\.0",
)
.unwrap()
});
pub struct DnsRebindingDetector;
impl Detector for DnsRebindingDetector {
fn name(&self) -> &'static str {
"dns_rebinding"
}
fn detect(&self, input: &str) -> Option<DetectionResult> {
regex_detect(
std::slice::from_ref(&*WEAK_PATTERNS),
self.name(),
AttackCategory::Protocol,
Severity::Low,
"DNS rebinding signal: Host header names a private address (weak signal)",
input,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_low(input: &str) {
crate::test_helpers::assert_detected(
&DnsRebindingDetector,
input,
AttackCategory::Protocol,
Severity::Low,
);
}
fn assert_clean(input: &str) {
crate::test_helpers::assert_clean(&DnsRebindingDetector, input);
}
#[test]
fn name_is_dns_rebinding() {
assert_eq!(DnsRebindingDetector.name(), "dns_rebinding");
}
#[test]
fn detects_loopback_host() {
assert_low("Host: 127.0.0.1");
}
#[test]
fn detects_private_hosts() {
assert_low("Host: 10.0.0.2");
assert_low("Host: 192.168.1.1");
assert_low("Host: 172.16.0.1");
assert_low("Host: 172.31.255.255");
}
#[test]
fn detects_local_names() {
assert_low("Host: localhost");
assert_low("Host: [::1]");
assert_low("Host: 0.0.0.0");
}
#[test]
fn detects_mixed_case() {
assert_low("host: 127.0.0.1");
}
#[test]
fn intranet_host_headers_are_weak_not_high() {
for input in [
"GET /healthz HTTP/1.1\r\nHost: 10.244.1.5:8080\r\n\r\n",
"POST /v1/items HTTP/1.1\r\nHost: 172.18.0.2:3000\r\n\r\n",
"GET / HTTP/1.1\r\nHost: 192.168.1.1\r\n\r\n",
"GET /metrics HTTP/1.1\r\nHost: localhost:8000\r\n\r\n",
"GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
] {
assert_low(input);
}
}
#[test]
fn proxied_public_request_is_clean() {
assert_clean(
"GET /index.html HTTP/1.1\r\nHost: example.com\r\nX-Forwarded-For: 203.0.113.7\r\n\r\n",
);
}
#[test]
fn rejects_public_hosts() {
assert_clean("Host: example.com");
assert_clean("Host: 8.8.8.8");
assert_clean("Host: 172.32.0.1");
}
#[test]
fn rejects_hostname_variants() {
assert_clean("Hostname: 127.0.0.1");
assert_clean("Hostname: localhost");
}
#[test]
fn rejects_near_misses() {
assert_clean("Host: 12.7.0.1");
assert_clean("Host: 172.15.0.1");
}
#[test]
fn rejects_empty_and_whitespace() {
assert_clean("");
assert_clean(" ");
}
#[test]
fn rejects_unicode_text() {
assert_clean("主机名解析测试,无攻击");
}
}