Skip to main content

security_rust/data/
jwt_attack.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
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(
27            &PATTERNS,
28            self.name(),
29            AttackCategory::Data,
30            Severity::High,
31            "JWT attack detected",
32            input,
33        )
34    }
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40
41    #[test]
42    fn name_returns_attack_type() {
43        assert_eq!(JwtAttackDetector.name(), "jwt_attack");
44    }
45
46    #[test]
47    fn detects_none_algorithm() {
48        for payload in [
49            r#"{"alg": "none", "typ": "JWT"}"#,
50            r#"{"alg":"None"}"#,
51            r#"{"alg": "NONE"}"#,
52            r#"{"alg": "noNe"}"#,
53        ] {
54            let r = JwtAttackDetector
55                .detect(payload)
56                .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
57            assert_eq!(r.attack_type, "jwt_attack");
58            assert_eq!(r.category, AttackCategory::Data);
59            assert_eq!(r.severity, Severity::High);
60            assert!(
61                !r.matched_pattern.is_empty(),
62                "matched_pattern empty for {:?}",
63                payload
64            );
65            assert!(
66                r.offset <= payload.len(),
67                "offset out of range for {:?}",
68                payload
69            );
70        }
71    }
72
73    #[test]
74    fn detects_kid_traversal() {
75        for payload in [
76            r#"{"kid": "../../etc/passwd"}"#,
77            r#"{"kid": "/dev/null"}"#,
78            r#"{"kid": "..\\..\\key"}"#,
79        ] {
80            let r = JwtAttackDetector
81                .detect(payload)
82                .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
83            assert!(
84                !r.matched_pattern.is_empty(),
85                "matched_pattern empty for {:?}",
86                payload
87            );
88            assert!(
89                r.offset <= payload.len(),
90                "offset out of range for {:?}",
91                payload
92            );
93        }
94    }
95
96    #[test]
97    fn detects_token_shape() {
98        for payload in [
99            "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.abc123",
100            "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.",
101            "eyJhbGciOiJIUzI1NiJ9. .abc123",
102        ] {
103            let r = JwtAttackDetector
104                .detect(payload)
105                .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
106            assert!(
107                !r.matched_pattern.is_empty(),
108                "matched_pattern empty for {:?}",
109                payload
110            );
111            assert!(
112                r.offset <= payload.len(),
113                "offset out of range for {:?}",
114                payload
115            );
116        }
117    }
118
119    #[test]
120    fn ignores_benign_inputs() {
121        for input in [
122            "Hello, this is a normal text input.",
123            r#"{"alg": "HS256"}"#,
124            r"{'alg': 'none'}",
125            r#"{"kid": "key-1"}"#,
126            "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ",
127            "The eye color is blue",
128        ] {
129            assert!(
130                JwtAttackDetector.detect(input).is_none(),
131                "false positive: {:?}",
132                input
133            );
134        }
135    }
136
137    #[test]
138    fn edge_cases() {
139        assert!(JwtAttackDetector.detect("").is_none());
140        assert!(JwtAttackDetector.detect("   ").is_none());
141        assert!(JwtAttackDetector.detect("алгоритм: none").is_none());
142    }
143}