fix(verify): the app check accepts a signed record edited to add a prototype key; verify.sh rejects it#1674

opened28d agobyBbirdbrain-agent

Defect: the app's integrity check accepts a signed record edited to add a hidden key, while the package's own checking script rejects it.

What: If someone with access to a case folder adds one specially named key (the JavaScript prototype name) inside a capture's recorded headers or a thumbnail record's parameters after the record was signed, the in-app check and the standalone checker still report the history intact, while the checking script bundled in an exported package reports that record as altered. The added content is inert, since nothing reads it, but the two checkers disagree on the same package and the app calls an edited record unedited. Found by the automated review of the thumbnail record change and kept out of it because the fix belongs in the shared checker and also covers capture headers, which predate that change.

Where it is and how to reproduce it

Where: src/shared/verify/manifestChain.ts:249-252 (hashes the schema-parsed entry, not the raw line); src/shared/schemas.ts:462 and :744-747 (z.record in zod 4.6.5 drops an own __proto__ key without validating it); src/main/services/verifyScript.ts:232 (jq -cS recomputes from the raw line, so it differs). Read on PR #1671's head 9d52f472; the headers surface is on origin/main. Reproduce: in a scratch test, sign a capture entry, then rewrite its line adding "__proto__":{"note":"x"} inside headers. verifyManifestChainText returns {"valid":true}; the verifyScript.ts:232 recipe gives a different hash from the stored entryHash. The reviewer's probe: 3cb0dee7… against stored 34c017f2… (capture), 05990d4f… against 5821d166… (derivation). Suggested direction: hash the raw parsed line, or reject own __proto__ keys, in the verify core.

1pnpm test tests/shared/verify/manifestChain.test.ts

This was generated by AI during triage.

Triage: bug, ready for an agent, and marked evidence-affecting.

I confirmed the mechanism on current main. The parser the shared checker uses silently drops a key with the JavaScript prototype name before the record is hashed, so the in-app check and the standalone checker call an edited record unedited, while the script bundled in the package rejects it. The fix belongs in the shared checker. The brief below is the contract for the work.

Agent brief

Agent Brief

Category: bug Summary: The shared manifest-chain verifier must report a signed entry as altered when an own __proto__ key was added to it after signing.

Current behavior: verifyManifestChainText parses each line with JSON.parse, validates it with ManifestEntrySchema.safeParse, and recomputes entryHash from the schema-parsed result. Under zod 4.6.5, z.record(...) drops an own __proto__ key without validating it (confirmed: z.record(z.string(), z.string()).safeParse(JSON.parse('{"a":"1","__proto__":{"note":"x"}}')) returns { success: true, data: { a: "1" } }). So an entry edited to add "__proto__": {...} inside a capture's headers, or a derivation entry's record-typed params, rehashes to its stored entryHash and the chain is reported valid. The bundled verify.sh recomputes from the raw line with jq -cS and reports the same entry altered, so the two checkers disagree on one package.

Desired behavior: The verify core reports that entry as an entry-hash mismatch (chain broken at that index), agreeing with verify.sh. Either hash the raw parsed line rather than the schema output, or reject an own __proto__ key anywhere in an entry before hashing; the agent picks, but the result must hold for every record-typed field, not only headers. Watch that whatever canonical serialization the hash uses actually emits an own __proto__ property (object spread and JSON.stringify do; assignment does not).

Key interfaces:

  • verifyManifestChainText and every path in the shared verify core that recomputes entryHash (there is more than one loop; one single-entry path already destructures the raw record).
  • The standalone verifier and the in-app check both go through this core; no separate fix in either.

Acceptance criteria:

  • A test signs a capture entry, rewrites its line to add "__proto__":{"note":"x"} inside headers, and verifyManifestChainText reports the chain broken at that entry.
  • The same for an entry whose record-typed parameters gain an own __proto__ key.
  • Where jq is available, a test asserts the core and the verify.sh hash recipe agree on that edited line.
  • Every existing manifest-chain and package-verifier test passes unchanged; an unedited manifest still verifies.

Out of scope:

  • Upgrading or replacing zod.
  • Prototype-key hardening outside the verify path.
  • Any change to verify.sh or VERIFY.md, which already reject the edit.

Note

Claude responding on behalf of Matt.

Synced from GitHub

Closed as not planned by the stale-agent-issues workflow: filed by the machine account and untouched for 14 days (ADR-0028).

Reopen it and apply queued to put it on the dispatch list.

Note

Written by claude-opus-5-5 via Claude Code on behalf of Matt.

Reopening. The automatic stale-issue cleanup closed this without a fix, and a check of the current code on 23 October found the defect still present. It is on the new delivery roadmap's defect list; it is not queued for an agent yet.

Where the roadmap lists it

docs/plans/2026-10-23-delivery-roadmap.md, Defect lane items 1 and 2 (PR #1959). The cleanup's rule change that exempts evidence and security issues is PR #1957.