From 0612442158c207fa1f23ca171503bedf500d7ff2 Mon Sep 17 00:00:00 2001 From: Jenny Ferries Date: Wed, 29 Jul 2026 08:59:31 -0700 Subject: [PATCH] daily-focus-board: reject inherited-property-name task ids (prototype safety) Resolves the Copilot review on board-core.mjs: task ids like `toString` / `constructor` passed validId and, used as object keys, could resolve to Object.prototype members. Now validId also rejects any inherited name (`s in {}`), and the progress maps (counters, t) are null-prototype so a task id can never resolve to an inherited member. (`__proto__` was already blocked by the id grammar.) Headless test 43/43 (adds inherited-name rejection, drop-on-normalize, and an Object.prototype-intact check). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cb356aa8-0af2-48f3-b3c6-8086c69d5308 --- extensions/daily-focus-board/board-core.mjs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/extensions/daily-focus-board/board-core.mjs b/extensions/daily-focus-board/board-core.mjs index 35e1b5bc..afc85492 100644 --- a/extensions/daily-focus-board/board-core.mjs +++ b/extensions/daily-focus-board/board-core.mjs @@ -46,7 +46,12 @@ const BOARD_MARKER = "daily-focus-board"; // delete routing, so a task may not claim them as an id — otherwise deleting a // task note could splice the shared feed instead. const RESERVED_IDS = new Set(["day", "brain"]); -export function validId(s) { return typeof s === "string" && ID_RE.test(s) && !RESERVED_IDS.has(s); } +// Reject reserved sentinels AND any inherited Object.prototype name (toString, +// constructor, hasOwnProperty, __proto__, ...) via `s in {}` — task ids are used +// as object keys, so an inherited name could otherwise resolve to a prototype +// member. With the null-prototype progress maps in normalize(), a task id can +// never touch the prototype. +export function validId(s) { return typeof s === "string" && ID_RE.test(s) && !RESERVED_IDS.has(s) && !(s in {}); } function text(s, max = 2000) { return typeof s === "string" ? s.slice(0, max) : ""; } function num(v) { if (v === undefined || v === null || v === "") return undefined; @@ -89,8 +94,10 @@ export function normalize(doc) { doc.tasks = Array.isArray(doc.tasks) ? doc.tasks.filter(t => t && validId(t.id)).map(normalizeTaskDef) : []; const p = doc.progress && typeof doc.progress === "object" ? doc.progress : {}; - p.counters = p.counters && typeof p.counters === "object" ? p.counters : {}; - p.t = p.t && typeof p.t === "object" ? p.t : {}; + // Null-prototype maps so a task id can never resolve to an inherited member + // (e.g. reading p.t["toString"] returning Object.prototype.toString). + p.counters = Object.assign(Object.create(null), p.counters && typeof p.counters === "object" ? p.counters : {}); + p.t = Object.assign(Object.create(null), p.t && typeof p.t === "object" ? p.t : {}); p.day = Array.isArray(p.day) ? p.day : []; p.brain = Array.isArray(p.brain) ? p.brain : []; p.focus = validId(p.focus) ? p.focus : null;