security_rust/data/
jwt_attack.rs1use crate::{regex_detect, AttackCategory, DetectionResult, Detector, Severity};
4use regex::Regex;
5use std::sync::LazyLock;
6
7static PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
8 vec![
9 Regex::new(r#"(?i)"alg"\s*:\s*"none""#).unwrap(),
10 Regex::new(r#"(?i)"kid"\s*:.*\.\.\/"#).unwrap(),
11 Regex::new(r#"(?i)"kid"\s*:.*\.\.\\"#).unwrap(),
12 Regex::new(r#"(?i)"kid"\s*:.*/dev/null"#).unwrap(),
13 Regex::new(r"ey[A-Za-z0-9_-]+\.ey[A-Za-z0-9_-]+\.[\s]*").unwrap(),
14 Regex::new(r"ey[A-Za-z0-9_-]+\.[\s]*\.[A-Za-z0-9_-]+").unwrap(),
15 ]
16});
17
18pub struct JwtAttackDetector;
19
20impl Detector for JwtAttackDetector {
21 fn name(&self) -> &'static str {
22 "jwt_attack"
23 }
24
25 fn detect(&self, input: &str) -> Option<DetectionResult> {
26 regex_detect(&PATTERNS, self.name(), AttackCategory::Data, Severity::High, "JWT attack detected", input)
27 }
28}
29
30#[cfg(test)]
31mod tests {
32 use super::*;
33
34 #[test]
35 fn name_returns_attack_type() {
36 assert_eq!(JwtAttackDetector.name(), "jwt_attack");
37 }
38
39 #[test]
40 fn detects_none_algorithm() {
41 for payload in [
42 r#"{"alg": "none", "typ": "JWT"}"#,
43 r#"{"alg":"None"}"#,
44 r#"{"alg": "NONE"}"#,
45 r#"{"alg": "noNe"}"#,
46 ] {
47 let r = JwtAttackDetector
48 .detect(payload)
49 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
50 assert_eq!(r.attack_type, "jwt_attack");
51 assert_eq!(r.category, AttackCategory::Data);
52 assert_eq!(r.severity, Severity::High);
53 assert!(
54 !r.matched_pattern.is_empty(),
55 "matched_pattern empty for {:?}",
56 payload
57 );
58 assert!(
59 r.offset <= payload.len(),
60 "offset out of range for {:?}",
61 payload
62 );
63 }
64 }
65
66 #[test]
67 fn detects_kid_traversal() {
68 for payload in [
69 r#"{"kid": "../../etc/passwd"}"#,
70 r#"{"kid": "/dev/null"}"#,
71 r#"{"kid": "..\\..\\key"}"#,
72 ] {
73 let r = JwtAttackDetector
74 .detect(payload)
75 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
76 assert!(
77 !r.matched_pattern.is_empty(),
78 "matched_pattern empty for {:?}",
79 payload
80 );
81 assert!(
82 r.offset <= payload.len(),
83 "offset out of range for {:?}",
84 payload
85 );
86 }
87 }
88
89 #[test]
90 fn detects_token_shape() {
91 for payload in [
92 "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.abc123",
93 "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.",
94 "eyJhbGciOiJIUzI1NiJ9. .abc123",
95 ] {
96 let r = JwtAttackDetector
97 .detect(payload)
98 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
99 assert!(
100 !r.matched_pattern.is_empty(),
101 "matched_pattern empty for {:?}",
102 payload
103 );
104 assert!(
105 r.offset <= payload.len(),
106 "offset out of range for {:?}",
107 payload
108 );
109 }
110 }
111
112 #[test]
113 fn ignores_benign_inputs() {
114 for input in [
115 "Hello, this is a normal text input.",
116 r#"{"alg": "HS256"}"#,
117 r"{'alg': 'none'}",
118 r#"{"kid": "key-1"}"#,
119 "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ",
120 "The eye color is blue",
121 ] {
122 assert!(
123 JwtAttackDetector.detect(input).is_none(),
124 "false positive: {:?}",
125 input
126 );
127 }
128 }
129
130 #[test]
131 fn edge_cases() {
132 assert!(JwtAttackDetector.detect("").is_none());
133 assert!(JwtAttackDetector.detect(" ").is_none());
134 assert!(JwtAttackDetector.detect("алгоритм: none").is_none());
135 }
136}