use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
use regex::Regex;
use std::sync::LazyLock;
static PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(concat!(
r"'(?i:\s*or\s*'1'\s*=\s*'1)",
r"|'(?i:\s*and\s*'1'\s*=\s*'2)",
r"|'(?i:\s*or\s*1\s*=\s*1)",
r#"|"(?i:\s*or\s*"1"\s*=\s*"1)"#,
r"|'\s*\]\s*\|\s*[/@(*]",
r"|'(?i:\s*or\s*''=')",
r"|'(?i:\s*or\s*true\s*\()",
))
.unwrap()
});
pub struct XPathInjectionDetector;
impl Detector for XPathInjectionDetector {
fn name(&self) -> &'static str {
"xpath_injection"
}
fn detect(&self, input: &str) -> Option<DetectionResult> {
regex_detect(
std::slice::from_ref(&*PATTERNS),
self.name(),
AttackCategory::Injection,
Severity::High,
"XPATH injection detected",
input,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn det() -> XPathInjectionDetector {
XPathInjectionDetector
}
fn assert_hit(input: &str) {
crate::test_helpers::assert_detected(
&det(),
input,
AttackCategory::Injection,
Severity::High,
);
}
#[test]
fn name_is_xpath_injection() {
assert_eq!(det().name(), "xpath_injection");
}
#[test]
fn detects_common_payloads() {
for input in [
"' or '1'='1",
"' or 1=1",
"' and '1'='2",
r#"" or "1"="1"#,
"' or ''='",
"' or true()",
"']|//admin",
] {
assert_hit(input);
}
}
#[test]
fn benign_inputs_not_detected() {
for input in [
"Hello, this is a normal text input. Nothing suspicious here.",
"The first quarter results are in",
"or is a conjunction in English",
"I want 1 pizza and 1 drink",
] {
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("or 1=1").is_none());
assert!(det().detect("' or '2'='1").is_none());
assert!(det().detect("' or 2=2").is_none());
}
#[test]
fn obfuscated_variants_detected() {
for input in ["' OR '1'='1", "' And '1'='2", r#"" Or "1"="1"#] {
assert_hit(input);
}
}
#[test]
fn bracket_index_bitwise_or_is_not_detected() {
for input in [
"const flags = opts['flags'] | 0;\n",
"merged = d['a'] | d['b']\n",
"type Mode = flags['mode'] | Extra;\n",
] {
crate::test_helpers::assert_clean(&det(), input);
}
}
#[test]
fn union_with_path_still_detected() {
for input in [
"']|//admin",
"'] | /user[position()=1]",
"']|@secret-attr",
"']|*",
] {
assert_hit(input);
}
}
}