Skip to main content

security_rust/data/
jwt_attack.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3use 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}