{"record":{"id":"8a746998cab53161","repo":"JuliusBrussee/caveman","slug":"proposalrun-seq-d-row-hash-mismatch-tampered","errorCode":null,"errorMessage":"proposalrun: seq %d row_hash mismatch (tampered)","messagePattern":"proposalrun: seq (.+?) row_hash mismatch \\(tampered\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"shared/platform/proposalrun/proposalrun.go","lineNumber":364,"sourceCode":"\tsorted := make([]Run, len(runs))\n\tcopy(sorted, runs)\n\tsort.Slice(sorted, func(i, j int) bool { return sorted[i].Seq < sorted[j].Seq })\n\n\tprevHash := \"\"\n\tfor i, r := range sorted {\n\t\twantSeq := int64(i + 1)\n\t\tif r.Seq != wantSeq {\n\t\t\treturn fmt.Errorf(\"proposalrun: seq %d out of order (expected %d)\", r.Seq, wantSeq)\n\t\t}\n\t\tif r.PrevHash != prevHash {\n\t\t\treturn fmt.Errorf(\"proposalrun: seq %d prev_hash does not link to the prior row\", r.Seq)\n\t\t}\n\t\tgot, err := RowHash(r.PrevHash, r.Seq, r.Action, r.Detail, r.CostUSD, r.CreatedAt)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"proposalrun: seq %d recompute: %w\", r.Seq, err)\n\t\t}\n\t\tif got != r.RowHash {\n\t\t\treturn fmt.Errorf(\"proposalrun: seq %d row_hash mismatch (tampered)\", r.Seq)\n\t\t}\n\t\tprevHash = r.RowHash\n\t}\n\treturn nil\n}\n","sourceCodeStart":346,"sourceCodeEnd":370,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/platform/proposalrun/proposalrun.go#L346-L370","documentation":"The core tamper signal. VerifyChain recomputed the row's hash from its stored fields (PrevHash, Seq, Action, Detail, CostUSD, CreatedAt) and got a value different from the stored RowHash. Since every input is read from the row itself, a mismatch means at least one field was modified after the row was written without updating its hash — the chain's integrity guarantee is broken at this row and everything after it is unverifiable.","triggerScenarios":"Any out-of-band UPDATE to action, detail, cost_usd, or created_at on a proposal_runs row; a DB restore that lost precision (e.g. created_at truncated to milliseconds by a different engine, breaking the microsecond-pinned formatTime); floating-point cost_usd round-tripped through a column with different precision.","commonSituations":"Manual SQL 'fixes' by an operator; migrating the table through a system that coerces timestamptz or numeric types; a restore from a backup tool that doesn't preserve exact microsecond timestamps; genuine malicious tampering.","solutions":["Diff the stored row against a trusted copy (PITR / replica / off-site audit log) to identify which field changed and whether it was accidental or malicious.","If accidental and authorized, correct the field AND recompute row_hash, then re-hash every subsequent row — or accept the break and record it in the audit trail.","Prevent recurrence: revoke UPDATE on the table from the app role (INSERT/SELECT only) and verify the chain on a schedule so drift is caught at first occurrence."],"exampleFix":"// before\n-- operator 'fix'\nUPDATE proposal_runs SET cost_usd = 0.42 WHERE seq = 7;  -- row_hash now stale -> mismatch\n\n// after\n-- make the table append-only for the app role\nREVOKE UPDATE ON proposal_runs FROM app_role;\n-- corrections go through the package API, which re-chains rows","handlingStrategy":"try-catch","validationCode":"// Optional early self-check: recompute a row's hash before trusting it downstream.\nfunc rowSelfConsistent(r proposalrun.Run) bool {\n    got, err := proposalrun.RowHash(r.PrevHash, r.Seq, r.Action, r.Detail, r.CostUSD, r.CreatedAt)\n    return err == nil && got == r.RowHash\n}","typeGuard":null,"tryCatchPattern":"if err := proposalrun.VerifyChain(runs); err != nil {\n    if strings.Contains(err.Error(), \"row_hash mismatch\") {\n        // treat as security incident: snapshot the row, diff against PITR/replica, alert\n    }\n}","preventionTips":["Make the table append-only (REVOKE UPDATE) so post-write edits are impossible via the app role.","Run VerifyChain after every batch of appends or on a schedule; drift caught early limits the re-chain scope.","Preserve exact types on migration/restore: timestamptz microseconds and numeric precision are hash inputs."],"tags":["tamper-detection","hash-chain","security","data-integrity"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}