security_rust/data/
prototype_pollution.rs1use 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()); assert!(PrototypePollutionDetector.detect("Прототип").is_none());
117 }
118}