fix: round-2 loop-A fresh review (503c326 baseline) — B1 replay cap, A6 discount record, 2FA reissue+cooldown, notification flood, lockout saturation, VAT/refund-status consolidation

Round 2 Loop A fresh money/security/dup-mod review. 23 findings fixed:

MONEY:
- CRITICAL: B1 duplicate auto-refund gains an attempt cap (b1_attempts col, cap 3) —
  a rejected auto-refund no longer re-replays the expired key every sweep run
  (which minted a stacking unauthorized charge each time); FAILED-webhook
  demotion respects the cap; never re-replay a key whose B1 refund failed
- HIGH: A6 deposit_covered_by_discount skip path now APPLIES the eligible
  campaign discount rows immediately (capped) instead of skipping with no
  discount recorded — no more promised-discount-not-recorded overcharge
- MEDIUM: 2FA code burned by the SAVE gate is re-issued on failed
  new-card+save_card charges (re-issue guard now covers req.SaveCard)
- LOW: GetBookingPaymentSummary excludes tip rows from paidAmount (remaining
  now matches the authoritative tip-excluded balance)

SECURITY:
- MEDIUM: unacknowledged CRITICAL admin-notification flood capped (global cap
  on critical_payment_log + refresh_token_reuse rows)
- MEDIUM: 2FA reissue no longer bypasses the mint cooldown (Check no longer
  clears LastMintAt on gate-verify; cleared on terminal charge success)
- MEDIUM: twofa.StateFor map-saturation returns a shared permanently-locked
  state instead of a fresh 5-guess budget per request
- MEDIUM: ProgressiveRateLimit rejects 429 past maxProgressiveSleepDelayMs
  instead of sleeping unboundedly; login bcrypt concurrency semaphore added
- LOW: loginInProgress 409->429; webhook key-set/URL-unset startup check;
  email-verification per-user attempt counter

DUP/MOD:
- formatCurrency single source (frontend format.ts, 7 files consolidated);
  SquareRefundStatusToLocal single source (errors.go, all sites); admin
  audit-log helper dedup; SCA retry model unified (proactive on all 6
  surfaces); buyDailyTotal/daily-cap mirror via backend; lock TTL from
  backend; generateUUID at all card-form sites; magic numbers named
  (defaultPostgresHost, epsilon, fee constants); admin CASH + gift-card
  terminal charges now audited; DAV_SKIP_INIT documented in manuals

Verified: 26/26 dev + 24/24 prod (GO_TESTING=1, the CI condition), both vet
tags, frontend tests+build, env-docs 42/42.
This commit is contained in:
2026-08-22 00:34:50 +01:00
parent 4e64e32f09
commit 3866cc5963
36 changed files with 2032 additions and 719 deletions
+16 -7
View File
@@ -955,11 +955,13 @@ func TestTwoFAAttemptMap_InLockoutRecordNotEvicted(t *testing.T) {
}
}
// TestTwoFAAttemptMap_FullOfLockedOut_ReturnsTransient verifies the pathological
// case: when every entry is a locked-out in-window record (a flood), the map
// does NOT evict one and does NOT grow past the cap — the new user gets a
// transient, untracked state for this request instead.
func TestTwoFAAttemptMap_FullOfLockedOut_ReturnsTransient(t *testing.T) {
// TestTwoFAAttemptMap_FullOfLockedOut_ReturnsPermanentlyLocked verifies the
// pathological case: when every entry is a locked-out in-window record (a
// flood), the map does NOT evict one and does NOT grow past the cap — the new
// user gets the SHARED permanently-locked state instead of a transient state
// with a fresh guessing budget (Round 2 Loop A finding 3: the old per-request
// transient silently disabled the 5-attempt lockout exactly under attack).
func TestTwoFAAttemptMap_FullOfLockedOut_ReturnsPermanentlyLocked(t *testing.T) {
twofa.MapMu.Lock()
origMap := twofa.Map
origCap := twofa.MaxTrackedAttempts
@@ -983,8 +985,9 @@ func TestTwoFAAttemptMap_FullOfLockedOut_ReturnsTransient(t *testing.T) {
st := twoFAAttemptStateFor("new_user")
require.NotNil(t, st)
require.True(t, st.LockedOut(clock.Now()), "an untracked user under map saturation must be treated as permanently locked out")
if _, ok := twofa.Map["new_user"]; ok {
t.Error("expected the transient state NOT to be stored when the map is full of in-lockout records")
t.Error("expected the shared permanently-locked state NOT to be stored when the map is full of in-lockout records")
}
if len(twofa.Map) != 3 {
t.Errorf("expected all 3 locked-out records to survive, got %d", len(twofa.Map))
@@ -1099,7 +1102,13 @@ func TestTwoFA_ConcurrentCheckTwoFACode_NoDeadlock(t *testing.T) {
origMap := twofa.Map
origCap := twofa.MaxTrackedAttempts
twofa.Map = make(map[string]*twoFAAttemptState)
twofa.MaxTrackedAttempts = 64
// Cap high enough that the ~90 locked-out users this test accumulates
// (6 workers × 15 iters) never saturate the map: under saturation the
// shared permanently-locked fallback (finding 3) would hand fresh users a
// pre-locked state and every "attempt 1" would wrongly report locked out.
// Saturation itself is covered by
// TestTwoFAAttemptMap_FullOfLockedOut_ReturnsPermanentlyLocked.
twofa.MaxTrackedAttempts = 512
twofa.MapMu.Unlock()
t.Cleanup(func() {
twofa.MapMu.Lock()