You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
sec(approvals): hash approval tokens on purchase/ri_exchange paths #71
The commitment-laddering schema PR (LeanerCloud/cloud-commitments-cli#1355) stores its run approval token as a SHA-256 hash (ladder_runs.approval_token_hash, migration 000080), never in plaintext. The pre-existing approval-token columns on the older purchase / RI-exchange paths still store raw tokens and should be hardened the same way:
Storing raw approval tokens means a read of the DB (backup leak, SQL injection, over-broad read grant) hands an attacker working approval links. Hashing at rest closes that: the raw token lives only in the emailed approval link; the DB holds only its digest.
Scope
Add hashed columns (approval_token_hash TEXT) alongside the existing raw columns via an expand-contract migration (add hashed column, backfill on next issue/rotate, switch writers to hash-on-write, then drop the raw column in a later contract migration).
Change the writers on both paths to store sha256(token) and the verifiers to constant-time-compare the hash (crypto/subtle), matching the pattern the ladder email-approval PR (phase-3 PR-3) will introduce for ladder_runs.approval_token_hash.
Do not weaken token entropy or expiry; this is storage-at-rest hardening only.
Context
Split out of LeanerCloud/cloud-commitments-cli#1355 CR review (F3) so the pre-existing raw-token gap is tracked rather than silently masked. The ladder path already does the right thing; this brings the two older paths to parity.
Already fixed by #416 ("sec(api): hash approval and revocation tokens at rest", merged 2026-09-28 as 0c09e11). That PR said Closes #103, which reported the same raw-token columns, so this issue stayed open.
On current main:
Migration:000100_hash_approval_tokens_at_rest adds approval_token_hash to purchase_executions and ri_exchange_history, converts existing raw tokens to their SHA-256 digest (so pending approvals and links already sent by email keep working), and blanks the raw column.
Writers: every writer stores the hash (internal/config/approval_token.go).
Checks: every check goes through the constant-time config.ApprovalTokenMatches (crypto/subtle). That covers purchase approval, messages, revoke, and RI-exchange approve and reject.
Dropping the raw approval_token columns is the remaining contract step, tracked in #422.
Activity
fix(ladder): store approval token as hash not plaintext
IAmJSD commentedon Oct 19, 2026
Claiming: working on this
IAmJSD commentedon Oct 19, 2026
Already fixed by #416 ("sec(api): hash approval and revocation tokens at rest", merged 2026-09-28 as 0c09e11). That PR said
Closes #103, which reported the same raw-token columns, so this issue stayed open.On current main:
000100_hash_approval_tokens_at_restaddsapproval_token_hashtopurchase_executionsandri_exchange_history, converts existing raw tokens to their SHA-256 digest (so pending approvals and links already sent by email keep working), and blanks the raw column.internal/config/approval_token.go).config.ApprovalTokenMatches(crypto/subtle). That covers purchase approval, messages, revoke, and RI-exchange approve and reject.Dropping the raw
approval_tokencolumns is the remaining contract step, tracked in #422.