From 72ec49f92f097393dac8d165093127dfd1a9a7d1 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 04:31:08 +0000 Subject: [PATCH] fix(engine): bound the keys an effect object is probed for Unknown effect keys are found by reparsing the whole object once per key, so an effect with many unknown keys cost their number squared in cloning and parsing before it was refused. More than 32 keys is refused outright. --- crates/engine/src/cmd/props.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/engine/src/cmd/props.rs b/crates/engine/src/cmd/props.rs index 01f83bc85..3025c5c4a 100644 --- a/crates/engine/src/cmd/props.rs +++ b/crates/engine/src/cmd/props.rs @@ -85,10 +85,18 @@ pub fn effect_from(cmd: &str, p: &Value, key: &str) -> Result> { Ok(Some(effect)) } +/// Most keys an effect object may carry (the widest effect takes about a dozen). +const MAX_EFFECT_KEYS: usize = 32; + /// Refuse the keys of effect `given` that `parse` ignores, naming the keys the effect takes. Set to /// null, a key it reads fails to parse or changes the effect; a key it ignores changes nothing. fn refuse_ignored_effect_keys(cmd: &str, at: &str, given: &Value, parse: impl Fn(&Value) -> Option) -> Result<()> { let Some(fields) = given.as_object() else { return Ok(()) }; + // Each key is probed by reparsing the whole object without it: bounded, or a long list of + // unknown keys costs its length squared before it is refused. + if fields.len() > MAX_EFFECT_KEYS { + return Err(bad(cmd, format!("{at}: more than {MAX_EFFECT_KEYS} properties"))); + } let parsed = parse(given); let ignored: Vec<&str> = fields .iter()