security_rust/data/
prototype_pollution.rs1use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
4use regex::Regex;
5use std::sync::LazyLock;
6
7static STRONG_PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
10 Regex::new(
12 &[
13 r#"(?i)__proto__(?:["'\[\]])"#,
15 r"|__proto__\s*=[ \t]*[^=\s]",
19 r"|constructor\[",
20 r"|constructor\.prototype",
21 r"|__defineGetter__",
22 r"|__defineSetter__",
23 r"|__lookupGetter__",
24 r"|__lookupSetter__",
25 r"|hasOwnProperty\[",
26 ]
27 .concat(),
28 )
29 .unwrap()
30});
31
32static WEAK_PATTERNS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)__proto__").unwrap());
36
37pub struct PrototypePollutionDetector;
38
39impl Detector for PrototypePollutionDetector {
40 fn name(&self) -> &'static str {
41 "prototype_pollution"
42 }
43
44 fn detect(&self, input: &str) -> Option<DetectionResult> {
45 regex_detect(
46 std::slice::from_ref(&*STRONG_PATTERNS),
47 self.name(),
48 AttackCategory::Data,
49 Severity::High,
50 "JavaScript prototype pollution detected",
51 input,
52 )
53 .or_else(|| {
54 regex_detect(
55 std::slice::from_ref(&*WEAK_PATTERNS),
56 self.name(),
57 AttackCategory::Data,
58 Severity::Low,
59 "__proto__ referenced (weak signal)",
60 input,
61 )
62 })
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn name_returns_attack_type() {
72 assert_eq!(PrototypePollutionDetector.name(), "prototype_pollution");
73 }
74
75 #[test]
76 fn detects_proto_and_constructor_payloads() {
77 for payload in [
78 r#"{"__proto__": {"isAdmin": true}}"#,
79 r#"{"__proto__": {"polluted": true}}"#,
80 "obj.constructor.prototype.isAdmin = true",
81 "a[constructor[0]]",
82 "o[__proto__][isAdmin]",
83 ] {
84 let r = PrototypePollutionDetector
85 .detect(payload)
86 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
87 assert_eq!(r.attack_type, "prototype_pollution");
88 assert_eq!(r.category, AttackCategory::Data);
89 assert_eq!(r.severity, Severity::High);
90 assert!(
91 !r.matched_pattern.is_empty(),
92 "matched_pattern empty for {:?}",
93 payload
94 );
95 assert!(
96 r.offset <= payload.len(),
97 "offset out of range for {:?}",
98 payload
99 );
100 }
101 }
102
103 #[test]
104 fn detects_legacy_getter_setter_apis() {
105 for payload in [
106 "__defineGetter__('x', fn)",
107 "__defineSetter__('x', fn)",
108 "__lookupGetter__('x')",
109 "__lookupSetter__('x')",
110 "hasOwnProperty['isAdmin']",
111 ] {
112 let r = PrototypePollutionDetector
113 .detect(payload)
114 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
115 assert!(
116 !r.matched_pattern.is_empty(),
117 "matched_pattern empty for {:?}",
118 payload
119 );
120 assert!(
121 r.offset <= payload.len(),
122 "offset out of range for {:?}",
123 payload
124 );
125 }
126 }
127
128 #[test]
131 fn bare_proto_reference_is_low_key_shape_is_high() {
132 for payload in [
133 r#"{"__proto__": {"isAdmin": true}}"#,
134 r#"{"__proto__":{"polluted":1}}"#,
135 "o[__proto__][isAdmin]",
136 "obj.__proto__ = {}",
137 "?__proto__[x]=1",
138 ] {
139 let r = PrototypePollutionDetector
140 .detect(payload)
141 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
142 assert_eq!(r.severity, Severity::High, "payload {:?}", payload);
143 }
144 for input in [
146 "const p = obj.__proto__;",
147 "if (obj.__proto__ === Array.prototype) { init(); }",
148 "| **prototype_pollution** | `__proto__` 原型链污染 | High |",
149 ] {
150 let r = PrototypePollutionDetector
151 .detect(input)
152 .unwrap_or_else(|| panic!("expected weak detection for {:?}", input));
153 assert_eq!(r.severity, Severity::Low, "input {:?}", input);
154 }
155 }
156
157 #[test]
158 fn ignores_benign_inputs() {
159 for input in [
160 "Hello, this is a normal text input.",
161 "constructor",
162 "hasOwnProperty",
163 "proto",
164 "the prototype chain is a concept",
165 ] {
166 assert!(
167 PrototypePollutionDetector.detect(input).is_none(),
168 "false positive: {:?}",
169 input
170 );
171 }
172 }
173
174 #[test]
175 fn edge_cases() {
176 assert!(PrototypePollutionDetector.detect("").is_none());
177 assert!(PrototypePollutionDetector.detect(" ").is_none());
178 assert!(PrototypePollutionDetector.detect("__proto__").is_none()); assert!(PrototypePollutionDetector.detect("Прототип").is_none());
180 }
181}