Location
crates/librefang-kernel/src/approval.rs:789-809 (verify_totp_code_with_issuer)
crates/librefang-kernel/src/approval.rs:864 (verify_recovery_code)
crates/librefang-api/src/routes/system.rs:1765-1825 (/api/approvals/{id}/approve)
Threat model
An authenticated Admin (or anyone who phishes a single TOTP code via a malicious approval prompt) can use one 6-digit TOTP code to approve multiple distinct high-risk operations within the same 30-second window — verify_totp_code_with_issuer only calls totp.check_current(code) and returns true; there is no "consumed codes" set keyed on (secret, slot).
Worse, verify_recovery_code does a read-modify-write on vault_get/vault_set("totp_recovery_codes") with no lock; two concurrent POST /api/approvals/{id}/approve requests carrying the same recovery code both pass position(), both succeed, and both call vault_set("…", "[]") — the recovery code, which is supposed to be one-time, approves N requests in parallel.
Suggested fix
Add a Mutex<HashSet<(slot_index, code_hash)>> for TOTP replay tracking (evict entries older than step + skew), and serialize verify_recovery_code behind a per-vault async mutex so the read-modify-write is atomic.
Location
crates/librefang-kernel/src/approval.rs:789-809(verify_totp_code_with_issuer)crates/librefang-kernel/src/approval.rs:864(verify_recovery_code)crates/librefang-api/src/routes/system.rs:1765-1825(/api/approvals/{id}/approve)Threat model
An authenticated Admin (or anyone who phishes a single TOTP code via a malicious approval prompt) can use one 6-digit TOTP code to approve multiple distinct high-risk operations within the same 30-second window —
verify_totp_code_with_issueronly callstotp.check_current(code)and returns true; there is no "consumed codes" set keyed on(secret, slot).Worse,
verify_recovery_codedoes a read-modify-write onvault_get/vault_set("totp_recovery_codes")with no lock; two concurrentPOST /api/approvals/{id}/approverequests carrying the same recovery code both passposition(), both succeed, and both callvault_set("…", "[]")— the recovery code, which is supposed to be one-time, approves N requests in parallel.Suggested fix
Add a
Mutex<HashSet<(slot_index, code_hash)>>for TOTP replay tracking (evict entries older thanstep + skew), and serializeverify_recovery_codebehind a per-vault async mutex so the read-modify-write is atomic.