diff --git a/crates/engine/src/cmd/sketch_clipboard.rs b/crates/engine/src/cmd/sketch_clipboard.rs index c9764455..364f1851 100644 --- a/crates/engine/src/cmd/sketch_clipboard.rs +++ b/crates/engine/src/cmd/sketch_clipboard.rs @@ -242,6 +242,10 @@ fn paste_step(extent: f64) -> f64 { fn paste(s: &mut Session, p: &Value) -> Result { let cmd = "sketch.paste"; + // `p["sketch"] = …` below panics on anything but an object. + if !(p.is_object() || p.is_null()) { + return Err(bad(cmd, "params must be an object")); + } let Some(clip) = s.sketch_clipboard.clone() else { return Err(bad(cmd, "nothing copied (sketch.copy first)")) }; let target = target_sketch(s, p, cmd)?; let positioned = p.get("at").is_some() || p.get("translate").is_some(); diff --git a/crates/engine/src/cmd/sketch_clipboard_tests.rs b/crates/engine/src/cmd/sketch_clipboard_tests.rs index 86ec2e24..3399f442 100644 --- a/crates/engine/src/cmd/sketch_clipboard_tests.rs +++ b/crates/engine/src/cmd/sketch_clipboard_tests.rs @@ -231,3 +231,21 @@ fn cut_copies_and_deletes_in_one_step_and_pastes_back_in_place() { run(&mut s, "edit.undo", json!({})); assert_eq!(sketch(&s, sid), &before); } + +/// Parameters that are not a JSON object (a number, text, a list) are an error, not a panic: +/// `sketch.cut` and `sketch.paste` add keys to a copy of the parameters, and serde_json panics +/// when a key is set on anything but an object (#1536, found by `hostile_params_never_panic`). +#[test] +fn clipboard_commands_refuse_parameters_that_are_not_an_object() { + let (mut s, rect, _) = drawing(); + run(&mut s, "sketch.copy", json!({"entities": [rect[0].clone()]})); + for id in ["sketch.cut", "sketch.copy", "sketch.paste"] { + for hostile in [json!(true), json!(-1), json!(1e308), json!("nope"), json!([]), json!([0, 0, 0]), json!([{"x": 1}])] { + let result = s.execute(id, &hostile); + assert!(!matches!(result, Err(crate::EngineError::Internal(..))), "{id} {hostile} panicked inside the command: {result:?}"); + assert!(result.is_err(), "{id} {hostile} should be refused"); + } + } + // Nothing was cut or pasted by the refused calls. + assert!(s.execute("sketch.paste", &json!({})).is_ok(), "the clipboard still holds the copy"); +}