Development record
readTranscript opens the hook-supplied transcript path without O_NOFOLLOW or an over-cap probe, the one guarded read left off fsutil.ReadGuarded, so a symlinked transcript_path is followed and an over-cap file is stored silently truncated
issueresolvedminoriss-347
readTranscript opens the hook-supplied transcript path without O_NOFOLLOW or an over-cap probe, the one guarded read left off fsutil.ReadGuarded, so a symlinked transcript_path is followed and an over-cap file is stored silently truncated
Evidence
internal/surface/cli/cli.go:1211opens withos.O_RDONLY|syscall.O_NONBLOCKand noO_NOFOLLOW; theIsRegularcheck at:1221fstats the already-followed target. Reproduced end-to-end: a symlinkedtranscript_pathwas followed and the target bytes persisted to the history store. The size check at:1224then reads viaLimitReaderwith no cap+1 probe, so a file crossing the cap between fstat and read stores silently truncated — contradicting spc-4's refuse-whole invariant.- Every sibling guarded read carries
O_NOFOLLOW(internal/fsutil/fsutil.go:48, spec/rules/record stores); the user-typed operand path into the samehistory.Capturesink is alreadyfsutil.ReadGuardedviareadGuardedOperand(cli.go:917) — the more-trusted input is guarded, the hook input is not. - Refuter verdict: CONFIRMED substantive-minor (CWE-59 hardening/consistency; same-identity write, no privilege boundary). Leaf
O_NOFOLLOWcosts nothing legitimate (symlinked ancestors still resolve).
Frontmatter
| id | iss-347 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | minor |
.abcd/work/issues/resolved/iss-347-readtranscript-opens-the-hook-supplied.md
open on GitHub ↗ · commit history ↗
mentions4
- dcn-26082000000000022026-08-20 — bughunt round 2 (branch bughunt-b/round-2): captured the round-2 records (iss-346..iss-357, with the two colliding ids re-minted as iss-358/iss-359), fixed
- iss-383the history transcript store's read path is unguarded — listRecords and Read use raw os.ReadFile, so a planted FIFO wedges history list and history capture while the store flock is held, and a symlinked record is followed
- iss-2608211134281912Four untrusted-operand stdin readers (readIdeatePayload, readLessonsPayload, readSynthesisPayload, readSource) read exactly cap bytes via a bare io.LimitReader(cap), so an over-cap '-' payload is silently truncated into a severed prefix instead of refused, while each reader's file transport (readGuardedOperand) refuses the same bytes with fsutil.ErrTooBig. Worst on the history capture path: history.Capture performs no parse and no cap check, so an over-cap stdin transcript is stored as an 8 MiB prefix, reported 'stored', under a sha256 idempotency key computed over the prefix — breaking spc-4's refuse-whole invariant. Also lets an over-cap ideate verdict past both the MaxPayloadBytes cap gate and the dec.More() trailing-data gate at a crafted boundary. Unswept siblings of resolved iss-201/iss-347; every site's doc comment claims stdin parity with the file cap.
- spc-4start-the-transcript-clock