From 33aeeead9c58fa52915e7adf5abb544cb7ad5c66 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Wed, 19 Aug 2026 19:37:43 -0400 Subject: [PATCH] Record the adversarial pass: what held up, and the one shared design gap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the "what does this trust?" item. Records what was checked and held, so a later pass starts from the gaps rather than re-deriving the same conclusions: socket input bounds, the exec allowlist's single-parse guarantee, CORS exact-matching, that no path is ever built from entry data, and that the prototype-pollution-shaped writes in merge sit behind encryption that already requires the seed. The pass found one real defect (the config API, fixed separately) and one property that should not be fixed unilaterally: an unencrypted portable backup is unauthenticated. Its checksum and its fingerprint both live in the same attacker-supplied file, so they catch corruption and nothing else, while a seed-only backup is genuinely authenticated by its Fernet HMAC — and import treats them identically, then REPLACES the vault. Python does the same, so this is a shared design property rather than a port defect, and any of the three fixes has to land in both implementations at once. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN --- TODO.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index 516d48c6..f5611868 100644 --- a/TODO.md +++ b/TODO.md @@ -192,9 +192,53 @@ everything still open, in the order it should be tackled. Python's allowlist or document the API surfaces as non-identical; do not quietly leave it as an accident. (Found 2026-08-19 during the adversarial pass.) - - [ ] **Adversarial pass over TS-only surfaces**, framed as "what does + - [x] **Adversarial pass over TS-only surfaces**, framed as "what does this trust?" rather than "is this correct?" — the framing that - finds trust-boundary bugs rather than logic bugs. + finds trust-boundary bugs rather than logic bugs. Done + 2026-08-19. What held up, so a later pass need not re-derive it: + + - The agent socket bounds its input (512KB/line, 64 connections) + and validates op/cap/token/fingerprint/id/sink as strings + before comparing them. + - The exec allowlist is checked with `parseCommandSpec` on the + SAME array the spawn later parses, so there is no double-parse + gap. Every malformed `command` shape fails closed. Containment + is "which binary", never "what it does" — already documented at + the call site. + - CORS is exact-match against an allowlist that is empty by + default, echoes only allowlisted origins, and sets `Vary`. + - No path is ever built from ENTRY data; the one caller-supplied + path (document export) goes through `resolveWithinProfile`. + - Prototype-pollution-shaped writes (`obj[key] = value` over + parsed JSON) exist in the merge path, but merge input is + encrypted under the index key, so producing one requires the + seed already. + + It found one real defect — the config API accepting any key and + any value, fixed in the same session — and one property worth + recording rather than unilaterally changing, below. + - [ ] **DECISION TO RECORD: an unencrypted portable backup is + unauthenticated, and import accepts it silently.** With + `encryption_mode: "none"` the payload is used verbatim; the + checksum that "verifies" it and the `fingerprint` that passes the + profile check both live in the same attacker-supplied file, so + they detect corruption and nothing else. A `seed-only` backup is + genuinely authenticated (Fernet HMAC under a seed-derived key — + only the seed holder could have produced it), and import treats + the two identically. `POST /api/v1/vault/import` REPLACES the + vault, so a user talked into restoring a hostile file gets an + attacker-chosen index whole: entries that look like theirs, with + stored values the attacker knows. + + Python does exactly the same thing (`portable_backup.py`, the + `PortableMode.NONE` branch), so this is a shared design property + and NOT a port defect — which is also why it is recorded here + instead of being fixed on one side. Options, cheapest first: + report `encryption_mode` in the import response so the caller can + see what it got; require an explicit opt-in for `none` the way + the fingerprint mismatch already does; or drop the mode. Any of + them has to land in both implementations together. + (Found 2026-08-19 during the adversarial pass.) Original note follows. - [ ] **(superseded by the above) Independent security review of the TypeScript branch.** Review strategy (decided 2026-08-18): two AI families plus vectors, because