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