security_rust/protocol/
host_header.rs1use regex::Regex;
4use std::sync::LazyLock;
5
6use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
7
8static STRONG_PATTERNS: LazyLock<Regex> =
14 LazyLock::new(|| Regex::new(r"(?i)(?:^|\r\n)\s*Host:[^\r\n]*\r\n\s*Host:").unwrap());
15
16static WEAK_PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
31 Regex::new(r"(?i)\r\n.*X-Forwarded-Host|\r\n.*X-Original-URL|\r\n.*X-Rewrite-URL").unwrap()
32});
33
34pub struct HostHeaderDetector;
35
36impl Detector for HostHeaderDetector {
37 fn name(&self) -> &'static str {
38 "host_header"
39 }
40
41 fn detect(&self, input: &str) -> Option<DetectionResult> {
42 regex_detect(
43 std::slice::from_ref(&*STRONG_PATTERNS),
44 self.name(),
45 AttackCategory::Protocol,
46 Severity::High,
47 "Host header attack detected",
48 input,
49 )
50 .or_else(|| {
51 regex_detect(
52 std::slice::from_ref(&*WEAK_PATTERNS),
53 self.name(),
54 AttackCategory::Protocol,
55 Severity::Low,
56 "Host override header present (weak signal)",
57 input,
58 )
59 })
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 fn assert_detected(input: &str) {
68 assert_hit_at(input, Severity::High);
69 }
70
71 fn assert_hit_at(input: &str, severity: Severity) {
72 crate::test_helpers::assert_detected(
73 &HostHeaderDetector,
74 input,
75 AttackCategory::Protocol,
76 severity,
77 );
78 }
79
80 fn assert_clean(input: &str) {
81 crate::test_helpers::assert_clean(&HostHeaderDetector, input);
82 }
83
84 #[test]
85 fn name_is_host_header() {
86 assert_eq!(HostHeaderDetector.name(), "host_header");
87 }
88
89 #[test]
90 fn detects_x_forwarded_host() {
91 assert_hit_at(
93 "Host: example.com\r\nX-Forwarded-Host: evil.com",
94 Severity::Low,
95 );
96 }
97
98 #[test]
99 fn detects_duplicate_host_line() {
100 assert_detected("Host: example.com\r\nHost: evil.com");
102 assert_detected("GET / HTTP/1.1\r\nHost: a.com\r\nHost: evil.com");
103 assert_detected("GET / HTTP/1.1\r\nHost: a.com\r\n Host: evil.com");
105 }
106
107 #[test]
108 fn detects_x_original_url() {
109 assert_hit_at("Host: a.com\r\nX-Original-URL: /admin", Severity::Low);
110 }
111
112 #[test]
113 fn detects_x_rewrite_url() {
114 assert_hit_at("Host: a.com\r\nX-Rewrite-URL: /admin", Severity::Low);
115 }
116
117 #[test]
118 fn detects_x_forwarded_host_only() {
119 assert_hit_at("Host: a.com\r\nX-Forwarded-Host: evil.com", Severity::Low);
122 }
123
124 #[test]
125 fn detects_mixed_case() {
126 assert_hit_at(
127 "host: example.com\r\nx-forwarded-host: evil.com",
128 Severity::Low,
129 );
130 }
131
132 #[test]
136 fn proxied_requests_hit_only_the_weak_tier() {
137 for req in [
138 "GET /dashboard HTTP/1.1\r\nHost: app.example.com\r\nX-Forwarded-Host: app.example.com\r\nX-Forwarded-Proto: https\r\nX-Forwarded-For: 203.0.113.9\r\n\r\n",
140 "GET /private HTTP/1.1\r\nHost: app.example.com\r\nX-Original-URL: /index.html\r\nX-Forwarded-For: 203.0.113.9\r\n\r\n",
142 ] {
143 assert_hit_at(req, Severity::Low);
144 }
145 }
146
147 #[test]
148 fn rejects_single_header_lines() {
149 assert_clean("Host: example.com");
150 assert_clean("X-Forwarded-Host: evil.com");
151 assert_clean("Host: example.com\r\nAccept: */*");
152 }
153
154 #[test]
159 fn rejects_well_formed_requests() {
160 for req in [
161 "GET /index.html HTTP/1.1\r\nHost: example.com\r\nUser-Agent: curl/8.5.0\r\nAccept: */*\r\n",
162 "POST /upload HTTP/1.1\r\nHost: upload.example.com\r\nContent-Type: multipart/form-data; boundary=----x\r\nContent-Length: 1234\r\n\r\n",
163 "GET /a HTTP/1.1\r\nHost: example.com\r\n\r\nGET /b HTTP/1.1\r\nHost: example.com\r\n\r\n",
165 "GET /index.html HTTP/1.1\r\nHost: example.com\r\nX-Forwarded-For: 203.0.113.7\r\n\r\n",
168 "GET /index.html HTTP/1.1\r\nHost: example.com\r\nX-Forwarded-For: 203.0.113.7, 10.0.0.1\r\nX-Forwarded-Proto: https\r\nX-Real-IP: 203.0.113.7\r\n\r\n",
169 ] {
170 assert_clean(req);
171 }
172 assert_clean("The report was generated with SYSTEM \"production\" settings.");
174 assert_clean("Please cc: ops@example.com on the reply.");
175 assert_clean("/search?tag=rust&tag=security&page=2");
176 }
177
178 #[test]
179 fn rejects_lf_only_newlines() {
180 assert_clean("Host: example.com\nX-Forwarded-Host: evil.com");
181 }
182
183 #[test]
184 fn rejects_empty_and_whitespace() {
185 assert_clean("");
186 assert_clean(" ");
187 }
188
189 #[test]
190 fn rejects_unicode_text() {
191 assert_clean("主机头示例,无攻击");
192 }
193}