security_rust/protocol/
websocket.rs1use regex::Regex;
4use std::sync::LazyLock;
5
6use crate::{regex_detect, AttackCategory, DetectionResult, Detector, Severity};
7
8static PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
9 vec![
10 Regex::new(r"(?i)Upgrade:\s*websocket").unwrap(),
11 Regex::new(r"(?i)Sec-WebSocket-Key:").unwrap(),
12 Regex::new(r"(?i)Origin:\s*null.*Upgrade").unwrap(),
13 Regex::new(r"(?i)ws://").unwrap(),
14 ]
15});
16
17pub struct WebSocketDetector;
18
19impl Detector for WebSocketDetector {
20 fn name(&self) -> &'static str {
21 "websocket"
22 }
23
24 fn detect(&self, input: &str) -> Option<DetectionResult> {
25 regex_detect(&PATTERNS, self.name(), AttackCategory::Protocol, Severity::High, "WebSocket hijack attempt detected", input)
26 }
27}
28
29#[cfg(test)]
30mod tests {
31 use super::*;
32
33 fn assert_detected(input: &str) {
34 crate::test_helpers::assert_detected(
35 &WebSocketDetector,
36 input,
37 AttackCategory::Protocol,
38 Severity::High,
39 );
40 }
41
42 fn assert_clean(input: &str) {
43 crate::test_helpers::assert_clean(&WebSocketDetector, input);
44 }
45
46 #[test]
47 fn name_is_websocket() {
48 assert_eq!(WebSocketDetector.name(), "websocket");
49 }
50
51 #[test]
52 fn detects_upgrade_header() {
53 assert_detected("Upgrade: websocket");
54 }
55
56 #[test]
57 fn detects_sec_websocket_key() {
58 assert_detected("Connection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==");
59 }
60
61 #[test]
62 fn detects_ws_scheme() {
63 assert_detected("ws://evil.com/socket");
64 }
65
66 #[test]
67 fn detects_null_origin_upgrade() {
68 assert_detected("Origin: null Upgrade: websocket");
69 }
70
71 #[test]
72 fn detects_mixed_case() {
73 assert_detected("upgrade: WebSocket");
74 assert_detected("SEC-WEBSOCKET-KEY: abc==");
75 }
76
77 #[test]
78 fn rejects_benign_requests() {
79 assert_clean("Upgrade: h2c");
80 assert_clean("Sec-WebSocket-Protocol: chat");
81 assert_clean("wss://example.com/socket");
82 assert_clean("Origin: https://example.com");
83 assert_clean("GET /chat HTTP/1.1\r\nConnection: keep-alive");
84 }
85
86 #[test]
87 fn rejects_empty_and_whitespace() {
88 assert_clean("");
89 assert_clean(" ");
90 }
91
92 #[test]
93 fn rejects_unicode_text() {
94 assert_clean("WebSocket 握手信息");
95 }
96}