use std::sync::OnceLock;
use regex::RegexSet;
static PROTO_PATTERNS: OnceLock<RegexSet> = OnceLock::new();
static PROTO_DESCRIPTIONS: &[&str] = &[
"Prototype pollution: __proto__",
"Prototype pollution: constructor.prototype",
"Prototype pollution: Object.assign with __proto__",
];
fn patterns() -> &'static RegexSet {
PROTO_PATTERNS.get_or_init(|| {
RegexSet::new([
r"__proto__",
r"(?i)constructor\s*[\[.]\s*prototype",
r#"(?i)"__proto__"\s*:"#,
])
.expect("prototype pollution regex patterns must compile")
})
}
pub fn check_prototype(input: &str) -> Option<String> {
let set = patterns();
let matches: Vec<_> = set.matches(input).into_iter().collect();
if matches.is_empty() {
None
} else {
let idx = matches[0];
Some(
PROTO_DESCRIPTIONS
.get(idx)
.unwrap_or(&"prototype pollution")
.to_string(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_proto() {
assert!(check_prototype("__proto__[isAdmin]=true").is_some());
assert!(check_prototype(r#"{"__proto__": {"isAdmin": true}}"#).is_some());
}
#[test]
fn detects_constructor_prototype() {
assert!(check_prototype("constructor.prototype.isAdmin=true").is_some());
assert!(check_prototype("constructor[prototype][isAdmin]=1").is_some());
}
#[test]
fn allows_normal_json() {
assert!(check_prototype(r#"{"name":"John","age":30}"#).is_none());
}
#[test]
fn allows_normal_path() {
assert!(check_prototype("/api/users/123").is_none());
}
}