Skip to main content

security_rust/data/
prototype_pollution.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
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(
29            &PATTERNS,
30            self.name(),
31            AttackCategory::Data,
32            Severity::High,
33            "JavaScript prototype pollution detected",
34            input,
35        )
36    }
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn name_returns_attack_type() {
45        assert_eq!(PrototypePollutionDetector.name(), "prototype_pollution");
46    }
47
48    #[test]
49    fn detects_proto_and_constructor_payloads() {
50        for payload in [
51            r#"{"__proto__": {"isAdmin": true}}"#,
52            r#"{"__proto__": {"polluted": true}}"#,
53            "obj.constructor.prototype.isAdmin = true",
54            "a[constructor[0]]",
55            "o[__proto__][isAdmin]",
56        ] {
57            let r = PrototypePollutionDetector
58                .detect(payload)
59                .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
60            assert_eq!(r.attack_type, "prototype_pollution");
61            assert_eq!(r.category, AttackCategory::Data);
62            assert_eq!(r.severity, Severity::High);
63            assert!(
64                !r.matched_pattern.is_empty(),
65                "matched_pattern empty for {:?}",
66                payload
67            );
68            assert!(
69                r.offset <= payload.len(),
70                "offset out of range for {:?}",
71                payload
72            );
73        }
74    }
75
76    #[test]
77    fn detects_legacy_getter_setter_apis() {
78        for payload in [
79            "__defineGetter__('x', fn)",
80            "__defineSetter__('x', fn)",
81            "__lookupGetter__('x')",
82            "__lookupSetter__('x')",
83            "hasOwnProperty['isAdmin']",
84        ] {
85            let r = PrototypePollutionDetector
86                .detect(payload)
87                .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
88            assert!(
89                !r.matched_pattern.is_empty(),
90                "matched_pattern empty for {:?}",
91                payload
92            );
93            assert!(
94                r.offset <= payload.len(),
95                "offset out of range for {:?}",
96                payload
97            );
98        }
99    }
100
101    #[test]
102    fn ignores_benign_inputs() {
103        for input in [
104            "Hello, this is a normal text input.",
105            "constructor",
106            "hasOwnProperty",
107            "proto",
108            "the prototype chain is a concept",
109        ] {
110            assert!(
111                PrototypePollutionDetector.detect(input).is_none(),
112                "false positive: {:?}",
113                input
114            );
115        }
116    }
117
118    #[test]
119    fn edge_cases() {
120        assert!(PrototypePollutionDetector.detect("").is_none());
121        assert!(PrototypePollutionDetector.detect("   ").is_none());
122        assert!(PrototypePollutionDetector.detect("__proto__").is_none()); // fullwidth underscores
123        assert!(PrototypePollutionDetector.detect("Прототип").is_none());
124    }
125}