Skip to main content

security_rust/data/
prototype_pollution.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3use crate::{regex_detect, AttackCategory, DetectionResult, Detector, Severity};
4use regex::Regex;
5use std::sync::LazyLock;
6
7static PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
8    vec![
9        Regex::new(r"(?i)__proto__").unwrap(),
10        Regex::new(r"(?i)constructor\[").unwrap(),
11        Regex::new(r"(?i)constructor\.prototype").unwrap(),
12        Regex::new(r"(?i)__defineGetter__").unwrap(),
13        Regex::new(r"(?i)__defineSetter__").unwrap(),
14        Regex::new(r"(?i)__lookupGetter__").unwrap(),
15        Regex::new(r"(?i)__lookupSetter__").unwrap(),
16        Regex::new(r"(?i)hasOwnProperty\[").unwrap(),
17    ]
18});
19
20pub struct PrototypePollutionDetector;
21
22impl Detector for PrototypePollutionDetector {
23    fn name(&self) -> &'static str {
24        "prototype_pollution"
25    }
26
27    fn detect(&self, input: &str) -> Option<DetectionResult> {
28        regex_detect(&PATTERNS, self.name(), AttackCategory::Data, Severity::High, "JavaScript prototype pollution detected", input)
29    }
30}
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35
36    #[test]
37    fn name_returns_attack_type() {
38        assert_eq!(PrototypePollutionDetector.name(), "prototype_pollution");
39    }
40
41    #[test]
42    fn detects_proto_and_constructor_payloads() {
43        for payload in [
44            r#"{"__proto__": {"isAdmin": true}}"#,
45            r#"{"__proto__": {"polluted": true}}"#,
46            "obj.constructor.prototype.isAdmin = true",
47            "a[constructor[0]]",
48            "o[__proto__][isAdmin]",
49        ] {
50            let r = PrototypePollutionDetector
51                .detect(payload)
52                .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
53            assert_eq!(r.attack_type, "prototype_pollution");
54            assert_eq!(r.category, AttackCategory::Data);
55            assert_eq!(r.severity, Severity::High);
56            assert!(
57                !r.matched_pattern.is_empty(),
58                "matched_pattern empty for {:?}",
59                payload
60            );
61            assert!(
62                r.offset <= payload.len(),
63                "offset out of range for {:?}",
64                payload
65            );
66        }
67    }
68
69    #[test]
70    fn detects_legacy_getter_setter_apis() {
71        for payload in [
72            "__defineGetter__('x', fn)",
73            "__defineSetter__('x', fn)",
74            "__lookupGetter__('x')",
75            "__lookupSetter__('x')",
76            "hasOwnProperty['isAdmin']",
77        ] {
78            let r = PrototypePollutionDetector
79                .detect(payload)
80                .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
81            assert!(
82                !r.matched_pattern.is_empty(),
83                "matched_pattern empty for {:?}",
84                payload
85            );
86            assert!(
87                r.offset <= payload.len(),
88                "offset out of range for {:?}",
89                payload
90            );
91        }
92    }
93
94    #[test]
95    fn ignores_benign_inputs() {
96        for input in [
97            "Hello, this is a normal text input.",
98            "constructor",
99            "hasOwnProperty",
100            "proto",
101            "the prototype chain is a concept",
102        ] {
103            assert!(
104                PrototypePollutionDetector.detect(input).is_none(),
105                "false positive: {:?}",
106                input
107            );
108        }
109    }
110
111    #[test]
112    fn edge_cases() {
113        assert!(PrototypePollutionDetector.detect("").is_none());
114        assert!(PrototypePollutionDetector.detect("   ").is_none());
115        assert!(PrototypePollutionDetector.detect("__proto__").is_none()); // fullwidth underscores
116        assert!(PrototypePollutionDetector.detect("Прототип").is_none());
117    }
118}