security_rust/protocol/
dns_rebinding.rs1use regex::Regex;
4use std::sync::LazyLock;
5
6use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
7
8static WEAK_PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
27 Regex::new(
28 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",
29 )
30 .unwrap()
31});
32
33pub struct DnsRebindingDetector;
34
35impl Detector for DnsRebindingDetector {
36 fn name(&self) -> &'static str {
37 "dns_rebinding"
38 }
39
40 fn detect(&self, input: &str) -> Option<DetectionResult> {
41 regex_detect(
42 std::slice::from_ref(&*WEAK_PATTERNS),
43 self.name(),
44 AttackCategory::Protocol,
45 Severity::Low,
46 "DNS rebinding signal: Host header names a private address (weak signal)",
47 input,
48 )
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 fn assert_low(input: &str) {
58 crate::test_helpers::assert_detected(
59 &DnsRebindingDetector,
60 input,
61 AttackCategory::Protocol,
62 Severity::Low,
63 );
64 }
65
66 fn assert_clean(input: &str) {
67 crate::test_helpers::assert_clean(&DnsRebindingDetector, input);
68 }
69
70 #[test]
71 fn name_is_dns_rebinding() {
72 assert_eq!(DnsRebindingDetector.name(), "dns_rebinding");
73 }
74
75 #[test]
76 fn detects_loopback_host() {
77 assert_low("Host: 127.0.0.1");
78 }
79
80 #[test]
81 fn detects_private_hosts() {
82 assert_low("Host: 10.0.0.2");
83 assert_low("Host: 192.168.1.1");
84 assert_low("Host: 172.16.0.1");
85 assert_low("Host: 172.31.255.255");
86 }
87
88 #[test]
89 fn detects_local_names() {
90 assert_low("Host: localhost");
91 assert_low("Host: [::1]");
92 assert_low("Host: 0.0.0.0");
93 }
94
95 #[test]
96 fn detects_mixed_case() {
97 assert_low("host: 127.0.0.1");
98 }
99
100 #[test]
104 fn intranet_host_headers_are_weak_not_high() {
105 for input in [
106 "GET /healthz HTTP/1.1\r\nHost: 10.244.1.5:8080\r\n\r\n",
107 "POST /v1/items HTTP/1.1\r\nHost: 172.18.0.2:3000\r\n\r\n",
108 "GET / HTTP/1.1\r\nHost: 192.168.1.1\r\n\r\n",
109 "GET /metrics HTTP/1.1\r\nHost: localhost:8000\r\n\r\n",
110 "GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
111 ] {
112 assert_low(input);
113 }
114 }
115
116 #[test]
118 fn proxied_public_request_is_clean() {
119 assert_clean(
120 "GET /index.html HTTP/1.1\r\nHost: example.com\r\nX-Forwarded-For: 203.0.113.7\r\n\r\n",
121 );
122 }
123
124 #[test]
125 fn rejects_public_hosts() {
126 assert_clean("Host: example.com");
127 assert_clean("Host: 8.8.8.8");
128 assert_clean("Host: 172.32.0.1");
129 }
130
131 #[test]
132 fn rejects_hostname_variants() {
133 assert_clean("Hostname: 127.0.0.1");
134 assert_clean("Hostname: localhost");
135 }
136
137 #[test]
138 fn rejects_near_misses() {
139 assert_clean("Host: 12.7.0.1");
140 assert_clean("Host: 172.15.0.1");
141 }
142
143 #[test]
144 fn rejects_empty_and_whitespace() {
145 assert_clean("");
146 assert_clean(" ");
147 }
148
149 #[test]
150 fn rejects_unicode_text() {
151 assert_clean("主机名解析测试,无攻击");
152 }
153}