Skip to content

sec(approvals): hash approval tokens on purchase/ri_exchange paths #71

Closed

Description

@cristim

Summary

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:

  • purchase_executions.approval_token (raw VARCHAR(255))
  • ri_exchange_history.approval_token (raw VARCHAR(255))

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

  1. 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).
  2. 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.
  3. 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.

Refs LeanerCloud/cloud-commitments-cli#1333

Activity

  1. IAmJSD commented on Oct 19, 2026

    @IAmJSD
    Collaborator

    Claiming: working on this

  2. self-assigned this
    on Oct 19, 2026
  3. IAmJSD commented on Oct 19, 2026

    @IAmJSD
    Collaborator

    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.

  4. removed their assignment
    on Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions