security-rust 3.0.0

A pure Rust attack detection library with 32 detectors across 4 categories — injection, protocol, data, and file — plus stateful session, throttling, and risk scoring. Zero framework dependencies.
Documentation
// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz

use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
use regex::Regex;
use std::sync::LazyLock;

/// 各分支合成一条 alternation。前四条是 `(?i)`,后两条**故意不是**——
/// JWT 的 `ey…` 前缀是大小写敏感的 base64url,故 flags 逐条包裹,不能提到最前面。
static PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(
        &[
            r#"(?i:"alg"\s*:\s*"none")"#,
            r#"|(?i:"kid"\s*:.*\.\.\/)"#,
            r#"|(?i:"kid"\s*:.*\.\.\\)"#,
            r#"|(?i:"kid"\s*:.*/dev/null)"#,
            r"|ey[A-Za-z0-9_-]+\.ey[A-Za-z0-9_-]+\.[\s]*",
            r"|ey[A-Za-z0-9_-]+\.[\s]*\.[A-Za-z0-9_-]+",
        ]
        .concat(),
    )
    .unwrap()
});

pub struct JwtAttackDetector;

impl Detector for JwtAttackDetector {
    fn name(&self) -> &'static str {
        "jwt_attack"
    }

    fn detect(&self, input: &str) -> Option<DetectionResult> {
        regex_detect(
            std::slice::from_ref(&*PATTERNS),
            self.name(),
            AttackCategory::Data,
            Severity::High,
            "JWT attack detected",
            input,
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn name_returns_attack_type() {
        assert_eq!(JwtAttackDetector.name(), "jwt_attack");
    }

    #[test]
    fn detects_none_algorithm() {
        for payload in [
            r#"{"alg": "none", "typ": "JWT"}"#,
            r#"{"alg":"None"}"#,
            r#"{"alg": "NONE"}"#,
            r#"{"alg": "noNe"}"#,
        ] {
            let r = JwtAttackDetector
                .detect(payload)
                .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
            assert_eq!(r.attack_type, "jwt_attack");
            assert_eq!(r.category, AttackCategory::Data);
            assert_eq!(r.severity, Severity::High);
            assert!(
                !r.matched_pattern.is_empty(),
                "matched_pattern empty for {:?}",
                payload
            );
            assert!(
                r.offset <= payload.len(),
                "offset out of range for {:?}",
                payload
            );
        }
    }

    #[test]
    fn detects_kid_traversal() {
        for payload in [
            r#"{"kid": "../../etc/passwd"}"#,
            r#"{"kid": "/dev/null"}"#,
            r#"{"kid": "..\\..\\key"}"#,
        ] {
            let r = JwtAttackDetector
                .detect(payload)
                .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
            assert!(
                !r.matched_pattern.is_empty(),
                "matched_pattern empty for {:?}",
                payload
            );
            assert!(
                r.offset <= payload.len(),
                "offset out of range for {:?}",
                payload
            );
        }
    }

    #[test]
    fn detects_token_shape() {
        for payload in [
            "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.abc123",
            "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.",
            "eyJhbGciOiJIUzI1NiJ9. .abc123",
        ] {
            let r = JwtAttackDetector
                .detect(payload)
                .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
            assert!(
                !r.matched_pattern.is_empty(),
                "matched_pattern empty for {:?}",
                payload
            );
            assert!(
                r.offset <= payload.len(),
                "offset out of range for {:?}",
                payload
            );
        }
    }

    #[test]
    fn ignores_benign_inputs() {
        for input in [
            "Hello, this is a normal text input.",
            r#"{"alg": "HS256"}"#,
            r"{'alg': 'none'}",
            r#"{"kid": "key-1"}"#,
            "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ",
            "The eye color is blue",
        ] {
            assert!(
                JwtAttackDetector.detect(input).is_none(),
                "false positive: {:?}",
                input
            );
        }
    }

    #[test]
    fn edge_cases() {
        assert!(JwtAttackDetector.detect("").is_none());
        assert!(JwtAttackDetector.detect("   ").is_none());
        assert!(JwtAttackDetector.detect("алгоритм: none").is_none());
    }
}