diff --git a/.abcd/work/DECISIONS.md b/.abcd/work/DECISIONS.md index c96bd8c..911d3c7 100644 --- a/.abcd/work/DECISIONS.md +++ b/.abcd/work/DECISIONS.md @@ -12,3 +12,4 @@ Dated, one-line summaries of each autonomous security-hardening round. - 2026-08-13 — Round 8: fixed 5 confirmed defects — (1) the startup collection picker rendered discovered `collection.json` metadata (`name`/`description`/`itemCount`) straight into JSX as React children while `fetchCollectionMetadata` only checked `typeof data === "object"`, and the picker sits above every error boundary, so an attacker `/gh//` link (discovery auto-runs with no click) serving a non-primitive `name`/`description`/`itemCount` threw "Objects are not valid as a React child" and blanked the whole app, with the poisoned `name` also persisting into the source store; the discovery boundary now coerces all three to primitives. (2) `getDisplayableFields` and the platform `additionalFields` copy emitted one DOM row per entity key with no ceiling (the entity schema is `.loose()`), so a single entity carrying ~100k scalar keys mounted ~300k DOM nodes in one synchronous commit when its "More" overlay opened — both paths are now capped at 100, matching `MAX_MEDIA_PER_CARD`. (3) `validateForcedSettings` guarded `cardBackStyle` and `titleDisplayMode` against inverted allowlists (`plain|pattern|gradient` and `always|hover|never`) that matched neither the real `CardBackStyle` (`bitmap|svg|colour`) nor `TitleDisplayMode` (`truncate|wrap`) enums, so every honest author's forced value was silently dropped while out-of-enum values were accepted, persisted globally, and then rejected wholesale by the settings-export schema on reimport — bricking the user's own backup; the allowlists now match the real enums (this fixes the round-6/7 mismatch on its genuine grounds — the correctness drop plus the export-brick self-DoS — not the previously-refuted CSS-clamping claim, which remains inert). (4) a "replace"-mode settings import called `resetToDefaults()`, which cleared the one-time `hasAppliedCollectionDefaults` marker to false and re-armed the `CollectionDataContext` effect, so the active untrusted collection's `defaults` (including `fieldMapping`) immediately overwrote seven just-imported fields while the UI reported success; the marker is now preserved across the replace reset. (5) imported edits typed field values as `z.unknown()` and merged them raw over the source card, which is rendered as a React child, so an "edits backup" with an object/array-valued `title` threw on every grid render and persisted across reloads and collections; the import boundary now restricts edit field values to JSON primitives (the only shape the edit form produces). 9 reproduction tests added (985 → 994). Refuted: the `?reset=1` settings wipe (a documented, in-app-surfaced self-service reset that clears only the `itemdeck-settings` key — edits/themes/sources/plugins survive — and whose substring match has no realistic colliding URL) and the unbounded `collection.json` `maxVisibleCards` (`z.number().int().positive()` already blocks the harmful non-finite/`<1`/float inputs, and the sole consumer uses the value only as a downward cap min'd against the real card count, so a large value is inert rather than a DoS). Nitpicks/defence-in-depth noted (not fixed): the `?reset=1` substring test and the missing `.max()` on the v2 `maxVisibleCards` schema are minor hardening items; plus the previously-logged raw `edits[card.id]` inherited-key lookup, dead discovery/theme-loader code, and the dev-only `migrate-collection.ts` `parseInt`→`null` / hardcoded-licence correctness bugs. Dependency review: `npm audit --omit=dev` reports 0 advisories in the production tree; no dependency was changed. - 2026-08-13 — Round 9: fixed 9 confirmed defects — (1) an untrusted `collection.json` `relationships` record is unbounded and `type.field` keyed, so a record whose keys all share the primary type's prefix collapsed into one bucket and made both the resolve pass and the per-card rank pass `O(relationships × entities)` again (the round-7 grouping bounded only the per-entity rebuild); capped at `MAX_RELATIONSHIPS`, and the single-file entity-array paths (`{type}s.json`/`{type}.json`) now share the `MAX_ENTITY_IDS` ceiling the index path already enforced, removing the entity-count multiplier. (2) `useAvailableFields` walked every key of the sampled cards and turned each into an `