fix: round-2 loop-B adversarial (503c326 baseline) — B1 webhook race, APPROVED refund semantics, notification cap single-source, 2FA cooldown/StateFor hardening, register bcrypt semaphore
Round 2 Loop B red-team (money/security/dup-mod adversarial) findings on the full payments overhaul: MONEY: - HIGH: webhook COMPLETED promotion now resolves the B1 parent row (mirrors the re-poll resolveB1ParentFailed + till-sale clawback) — the sweep no longer re-replays an expired key into stacked unauthorized charges - HIGH: A6 deposit-with-discount clamp — chargeAmount capped to max(0, remaining-discount) for ALL discount cases; overflow guard compares against the discounted remaining - MED-HIGH: APPROVED refunds treated as NON-terminal at the webhook (event-driven, may still fail); payments call sites aligned; FAILED can now demote an APPROVED-then-failed row - MED: B1 refund transport-error fails the row + CRITICAL immediately (no 3-charge stacking) - MED: till_sales capped-fail surfaces the outstanding funding (gift_card_transactions trace) for manual reversal - MED: guest-bookings cash/gift-card terminal charges now audited (NULL target); audit reordered post-commit; cancellation refunds audited - MED: A6 no-discount skip-path returns campaign_fully_redeemed 400 (no success-shaped no-op); skip-path writes a marker row for idempotency SECURITY: - HIGH: notification cap centralized in adminnotify (MaxUnacknowledgedCriticalLogs) + applied at ALL insert sites (webhooks x2, jwt refresh_token_reuse, account erasure, sweep, twofa) with suppressed-insert logging; per-issue bucket for reissue alerts - MED-HIGH: twofa.StateFor saturated state made IMMUTABLE (LastMintAt writes are no-ops; no cross-user throttling); eviction never drops in-window count>0 records - MED: /register now uses the shared bcrypt semaphore (authBcryptSlots, 20) — botnet CPU burn bounded - MED: NAT collateral reduced (429-reject only at top progressive tier; lower tiers sleep) - MED: ClearMintCooldownForUser exposed for fresh-charge success; reissue cooldown-skip raises a capped alert - LOW: audit coverage gaps (reschedule fee forgiveness, gift-card transfer, clawback) closed DUP/MOD: - Frontend deposit-percent literals -> POLICY constants (10 sites); LOYALTY_DISCOUNT_RATE single-sourced; generateUUID adopted; admin PaymentModal overflow-tip confirm path added; £500 gift-card cap named Verified: 26/26 dev + 24/24 prod (CI condition), both vet tags, frontend tests+build, env-docs 42/42.
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
||||
"crussell/auth"
|
||||
"crussell/db"
|
||||
"crussell/handlers/payments"
|
||||
"crussell/internal/adminnotify"
|
||||
"crussell/internal/dav"
|
||||
"crussell/internal/s3"
|
||||
"crussell/internal/square"
|
||||
@@ -142,11 +143,21 @@ func InsertSquareErasureCriticalNotification(ctx context.Context, key string) {
|
||||
// the admin alert.
|
||||
actx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
// Round 2 Loop B finding 1: this is a 'critical_payment_log' insert site —
|
||||
// the same atomic global cap as every other. The pre-check logs the
|
||||
// suppression; the fold inside the INSERT enforces it atomically.
|
||||
if adminnotify.CriticalLogsCapExceeded(actx, db.Conn, "critical_payment_log") {
|
||||
slog.Error("failed to insert critical notification for failed Square erasure — unacknowledged 'critical_payment_log' queue at the cap", "key", key)
|
||||
return
|
||||
}
|
||||
tag, err := db.Conn.Exec(actx, `
|
||||
INSERT INTO admin_notifications (id, reason, booking_id, user_id, created_at)
|
||||
VALUES ($1, 'critical_payment_log'::admin_notification_reason, NULL, NULL, NOW())
|
||||
SELECT $1, 'critical_payment_log'::admin_notification_reason, NULL, NULL, NOW()
|
||||
WHERE (SELECT COUNT(*) FROM admin_notifications _an
|
||||
WHERE _an.reason = 'critical_payment_log'
|
||||
AND _an.acknowledged_at IS NULL) < $2
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`, squareErasureNotificationID(key))
|
||||
`, squareErasureNotificationID(key), adminnotify.MaxUnacknowledgedCriticalLogs)
|
||||
if err != nil {
|
||||
slog.Error("failed to insert critical notification for failed Square erasure", "key", key, "err", err)
|
||||
return
|
||||
|
||||
@@ -277,7 +277,7 @@ func SetupTwoFAHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
st.LastMintAt = now
|
||||
st.SetLastMintAtLocked(now)
|
||||
|
||||
resp := map[string]any{"message": "Code sent"}
|
||||
if !twoFARequired() {
|
||||
@@ -840,7 +840,7 @@ func EnsurePendingTwoFACode(r *http.Request, userID string, st *twoFAAttemptStat
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
st.LastMintAt = now
|
||||
st.SetLastMintAtLocked(now)
|
||||
return code, twoFAPendingExpiry, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user