Engine: macro approvals are keyed on the macro's canonical JSON, not a 64-bit hash

Approvals were stored as (document, FNV-1a hash of the macro's JSON).
FNV-1a is not collision resistant, so a crafted workbook could hold a
harmless macro and a different one with the same hash, and approving
the first would let the second run without asking.

The key is now the macro's canonical JSON text itself (object keys
sorted), so two different macros never share an approval.
macros::hash is replaced by macros::fingerprint.
This commit is contained in:
prxnvt
2026-10-22 02:26:32 -04:00
parent b51d588cac
commit 06bbcb504a
5 changed files with 21 additions and 12 deletions
+9
View File
@@ -486,6 +486,15 @@ fn macros_from_a_file_ask_before_running() {
assert_eq!(v(&s, "A1").display(), "ran");
// Approved once per macro content: the second run doesn't ask.
s.execute("macro.run", json!({"name": "Theirs"})).unwrap();
// The approval holds the macro's whole content, not a hash two macros could share.
let m = s.doc().unwrap().wb.macros[0].clone();
let doc = s.doc().unwrap().uid;
let key: (u64, String) = (doc, crate::macros::macro_json(&m).to_string());
assert!(s.macro_approvals.contains(&key));
// The JSON is canonical: the same macro with its keys in another order is the same macro.
let a = crate::macros::parse_macro(&json!({"name": "M", "steps": [{"command": "cell.set", "params": {"cell": "A1", "input": "x"}}]})).unwrap();
let b = crate::macros::parse_macro(&json!({"steps": [{"params": {"input": "x", "cell": "A1"}, "command": "cell.set"}], "name": "M"})).unwrap();
assert_eq!(crate::macros::fingerprint(&a), crate::macros::fingerprint(&b));
}
#[test]