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
This commit is contained in:
Jenny Ferries
2026-07-29 08:59:31 -07:00
parent 1f8b7c7ec3
commit 0612442158
+10 -3
View File
@@ -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;