Skip to content

fix(hash): bound Argon2 time + PBKDF2 iterations against an untrusted-hash CPU-DoS#67

Merged
xunholy merged 1 commit into
mainfrom
fix/hash-cost-dos
Jun 19, 2026
Merged

fix(hash): bound Argon2 time + PBKDF2 iterations against an untrusted-hash CPU-DoS#67
xunholy merged 1 commit into
mainfrom
fix/hash-cost-dos

Conversation

@xunholy

@xunholy xunholy commented Jun 19, 2026

Copy link
Copy Markdown
Owner

A resource-exhaustion DoS: the Argon2 and PBKDF2 verify paths take their cost parameters from the hash being verified (reachable over MCP and via hash_crack_dictionary), but the iteration/time cost had no upper bound.

  • Argon2: parseArgon2PHC capped memory (OOM) and parallelism is uint8-bounded, but t= was unbounded. A crafted $argon2id$v=19$m=65536,t=4294967295,p=1$… hash drives argon2.IDKey through ~4 billion passes — an unbounded CPU hang the memory cap doesn't catch. Compute had the same hole via uint32(intOr(...)): time=-1 wraps to ~4 billion and slips past the lower-bound-only check.
  • PBKDF2 (Django/Werkzeug): parseIter checked n>=1 but had no ceiling, so pbkdf2_sha256$999999999999$… spins ~10¹² HMACs on verify. Sibling of the Argon2 bug — internal/dcc2 and internal/phpass already cap at 2²⁴.

Fixes (all enforced before the expensive derivation)

  • argon2MaxTime = 1024 (hugely generous vs RFC 9106's t=1..3) checked in parseArgon2PHC (verify/crack) and compute. Compute now validates the signed ints first, then narrows to uint32 — so a negative value is rejected cleanly instead of wrapping to a huge cost.
  • maxPBKDF2Iterations = 1<<24 (~16.7M, far above any real config) in parseIter + Compute, matching the dcc2/phpass convention.

Tests

Reject the hostile params in ~0ms (proving no derivation runs) while legitimate params still compute. govulncheck clean; no new go.mod deps.

Gates

gofmt · go vet ./... · task lint 0 issues · task test:full (race) · task vuln — all green.

…-hash CPU-DoS

A resource-exhaustion DoS: the Argon2 and PBKDF2 verify paths take their cost
parameters from the hash being verified (reachable over MCP and via
hash_crack_dictionary), but the iteration/time cost had no upper bound.

  - Argon2: parseArgon2PHC capped memory (OOM) and parallelism is uint8-bounded,
    but t= was unbounded. A crafted "$argon2id$v=19$m=65536,t=4294967295,p=1$…"
    hash drives argon2.IDKey through ~4 billion passes — an unbounded CPU hang
    the memory cap does not catch. Compute had the same hole via uint32(intOr):
    time=-1 wraps to ~4 billion and slips past the lower-bound-only check.
  - PBKDF2 (Django/Werkzeug, internal/webpass): parseIter checked n>=1 but had
    no ceiling, so "pbkdf2_sha256$999999999999$…" spins ~10^12 HMACs on verify.
    Sibling of the Argon2 bug; internal/dcc2 and internal/phpass already cap at
    2^24.

Fixes, all enforced BEFORE the expensive derivation runs:
  - argon2MaxTime = 1024 (hugely generous vs the RFC 9106 t=1..3) checked in
    parseArgon2PHC (verify/crack) and compute. Compute now validates the signed
    ints first, then narrows to uint32 — so a negative value is rejected with a
    clear message instead of wrapping to a huge cost.
  - maxPBKDF2Iterations = 1<<24 (~16.7M, far above any real Django/Werkzeug
    config) checked in parseIter (verify) and Compute, matching the dcc2/phpass
    convention.

Tests reject the hostile params in ~0ms (proving no derivation runs) while
legitimate params still compute. govulncheck clean; no new go.mod deps.

gofmt / go vet / task lint (0 issues) / task test:full (race) all green.
@xunholy
xunholy merged commit 70875f0 into main Jun 19, 2026
10 of 11 checks passed
@xunholy
xunholy deleted the fix/hash-cost-dos branch June 19, 2026 22:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant