Skip to main content

security_rust/injection/
xpath_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
7/// 强信号:引号开头的布尔恒等式,以及带路径的联合查询 —— 都是受约束的攻击形态。
8/// 7 条分支合并成 1 条 alternation —— `regex_detect` 对列表里每条 `Regex` 各跑一次
9/// `find`,干净输入下 7 次全文扫描变 1 次。分支顺序 = 原 vec 顺序(同一位置上取最左
10/// 分支;与「按列表顺序取第一条命中的模式」相比偏移量可能不同,档位不变)。
11/// 内联 flag 一律裹进 `(?i:…)` —— 裸 `(?i)` 的作用域会蔓延到它后面拼进来的分支。
12static PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
13    Regex::new(concat!(
14        r"'(?i:\s*or\s*'1'\s*=\s*'1)",
15        r"|'(?i:\s*and\s*'1'\s*=\s*'2)",
16        r"|'(?i:\s*or\s*1\s*=\s*1)",
17        r#"|"(?i:\s*or\s*"1"\s*=\s*"1)"#,
18        // 联合查询:XPath 的 `|` 两侧都是节点集(以 `/`、`@`、`(` 或 `*` 开头)。
19        // 旧形态 `'\s*\]\s*\|\s*` 只认「`']` 后面有 `|`」,于是命中了 JS 的按位或
20        // 与 Python 的集合并 —— `opts['flags'] | 0`、`merged = d['a'] | d['b']`
21        // 都是正常代码,且判 High(单条即拒绝)。
22        r"|'\s*\]\s*\|\s*[/@(*]",
23        r"|'(?i:\s*or\s*''=')",
24        r"|'(?i:\s*or\s*true\s*\()",
25    ))
26    .unwrap()
27});
28
29pub struct XPathInjectionDetector;
30
31impl Detector for XPathInjectionDetector {
32    fn name(&self) -> &'static str {
33        "xpath_injection"
34    }
35
36    fn detect(&self, input: &str) -> Option<DetectionResult> {
37        regex_detect(
38            std::slice::from_ref(&*PATTERNS),
39            self.name(),
40            AttackCategory::Injection,
41            Severity::High,
42            "XPATH injection detected",
43            input,
44        )
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    fn det() -> XPathInjectionDetector {
53        XPathInjectionDetector
54    }
55
56    fn assert_hit(input: &str) {
57        crate::test_helpers::assert_detected(
58            &det(),
59            input,
60            AttackCategory::Injection,
61            Severity::High,
62        );
63    }
64
65    #[test]
66    fn name_is_xpath_injection() {
67        assert_eq!(det().name(), "xpath_injection");
68    }
69
70    #[test]
71    fn detects_common_payloads() {
72        for input in [
73            "' or '1'='1",
74            "' or 1=1",
75            "' and '1'='2",
76            r#"" or "1"="1"#,
77            "' or ''='",
78            "' or true()",
79            "']|//admin",
80        ] {
81            assert_hit(input);
82        }
83    }
84
85    #[test]
86    fn benign_inputs_not_detected() {
87        for input in [
88            "Hello, this is a normal text input. Nothing suspicious here.",
89            "The first quarter results are in",
90            "or is a conjunction in English",
91            "I want 1 pizza and 1 drink",
92        ] {
93            assert!(det().detect(input).is_none(), "false positive: {input}");
94        }
95    }
96
97    #[test]
98    fn edge_cases() {
99        assert!(det().detect("").is_none());
100        assert!(det().detect(" \t\n ").is_none());
101        assert!(det().detect("你好世界 こんにちは").is_none());
102        // near misses: quote missing or condition not satisfied
103        assert!(det().detect("or 1=1").is_none());
104        assert!(det().detect("' or '2'='1").is_none());
105        assert!(det().detect("' or 2=2").is_none());
106    }
107
108    #[test]
109    fn obfuscated_variants_detected() {
110        for input in ["' OR '1'='1", "' And '1'='2", r#"" Or "1"="1"#] {
111            assert_hit(input);
112        }
113    }
114
115    /// 反向对照:`']` 后面跟 `|` 在正常代码里是按位或 / 集合并 —— 收紧前
116    /// `'\s*\]\s*\|\s*` 把这三条都判 High(单条即拒绝)。
117    #[test]
118    fn bracket_index_bitwise_or_is_not_detected() {
119        for input in [
120            "const flags = opts['flags'] | 0;\n",
121            "merged = d['a'] | d['b']\n",
122            "type Mode = flags['mode'] | Extra;\n",
123        ] {
124            crate::test_helpers::assert_clean(&det(), input);
125        }
126    }
127
128    /// 收紧的另一半:联合查询的节点集形态必须照报。
129    #[test]
130    fn union_with_path_still_detected() {
131        for input in [
132            "']|//admin",
133            "'] | /user[position()=1]",
134            "']|@secret-attr",
135            "']|*",
136        ] {
137            assert_hit(input);
138        }
139    }
140}