v0.797.0
v0.797.0 — audit: VerifyChain reports a poisoned success value as a chain break
Fixed (safety — audit tamper-evidence robustness)
-
VerifyChain scanned the success column into a bare Go bool. Legitimate rows
always store 0/1 (schema DEFAULT 1), but success and entry_hash are
independent columns an attacker with direct SQLite write can set separately.
Setting success to NULL or an out-of-range integer (e.g. 2) makes rows.Scan
fail ("couldn't convert 2 into type bool"), so VerifyChain returns an error
and ABORTS the entire walk instead of the designed "chain broken at row N"
verdict. A single UPDATE ... SET success=NULL turns the forensic integrity
check into a self-inflicted DoS.This is fail-loud, not fail-open — it can never read a tampered log as
Valid=true — but it degrades tamper-evidence: an operator gets an opaque type
error instead of a pinpointed break, and the verifier can't report which rows
are broken. Fix: scan success as sql.NullInt64 (matching the sibling
duration_ms) and feed success.Int64 == 1 into chainHash. Behaviour-preserving
for legitimate 0/1 rows; any poisoned value now flows into the hash recompute,
fails the comparison, and is correctly reported as a chain break
(Valid=false, FirstBrokenID=id).
Found via the internal audit sweep of the query DSL + hash chain. The rest
verified sound: chain pre-image injectivity (length-prefixed fields), verify
recomputes rather than trusting the stored hash, reorder/deletion/truncation +
CheckpointAnchor detection, fully-parameterized WHERE builder, capped LIMIT,
writeMu + flock single-writer, CSV formula-injection guard.
Verified
- task ci green: golangci-lint 0 issues, go vet, build, task test:full (full
suite, -race) 0 fail, govulncheck clean. task eval 17/17. All five release
cross-compile targets build green. - TestVerifyChain_PoisonedSuccessReportsBreak (success=2 and success=NULL):
reports a break at the poisoned row without erroring; proven to fail pre-fix
(both aborted with a bool scan error).
Follow-up (LOW, banked): Query/QueryFiltered share the bare-bool scan, where a
NULL-success row is silently dropped from results; barely reachable, tracked
separately.