fix: loop-B adversarial (503c326 baseline) — IDEMPOTENCY_KEY_REUSED reclassified ambiguous, 2FA reissue fail-closed alerts, family-cache crash window, consolidation regression checks
Loop B red-team (money/security/dup-mod adversarial) findings on the full payments overhaul: - CRITICAL-ish: IDEMPOTENCY_KEY_REUSED (409) no longer classified as a definitive 402 in chargeFailureStatus — it means the ORIGINAL charge may have landed with a different body, so it is now AMBIGUOUS (503): the frontend keeps the same idempotency key, the pending row stays rescuable by the sweep (which already treated it as ambiguous), and the frontend no longer regenerates the key into a possible double charge. SCA verification-required codes remain definitive 402. - HIGH: reissueTwoFACodeAfterFailedCharge now writes a CRITICAL admin notification (insertCriticalPaymentNotification) when issuance is refused (missing pepper / unavailable delivery) instead of silently stranding the customer; documented that a pepper CHANGE invalidates all pending codes. - MEDIUM: family-alive cache invalidation crash window documented (invalidate-after- commit leaves up to 30s warm on a crash; the near-TTL DB re-check bounds it). - Consolidation regression checks (8b2fe3b helpers): writeChargeSnapshot guard preserved at all sites, postChargeRecheck identical, squareRefundStatusToLocal mappings verified, reissue fresh-only semantics confirmed at all 5 call sites. Verified: 26/26 dev packages, both vet tags, frontend tests + build, env-docs 42/42.
This commit is contained in:
@@ -250,6 +250,17 @@ func CleanupExpiredRefreshTokens(ctx context.Context) (int, error) {
|
||||
// ids are captured before the delete and invalidated after the commit: a
|
||||
// pre-commit invalidation could race a concurrent VerifyToken that
|
||||
// re-caches the still-present row as alive.
|
||||
//
|
||||
// RESIDUAL WINDOW (documented, Loop B finding 3): the invalidation is an
|
||||
// in-memory cache operation that CANNOT be in the same transaction as the
|
||||
// DELETE. A crash between the DELETE commit and the
|
||||
// auth.InvalidateFamilyAliveBatch call leaves the cache warm for up to
|
||||
// familyAliveCacheTTL, admitting a bound access token after its family was
|
||||
// killed. The verify path closes this gap itself: verifyFamilyAlive
|
||||
// (auth/jwt.go) re-validates an ALIVE cache verdict against the DB when it
|
||||
// is within familyAliveRecheckGrace of its TTL, bounding the residual
|
||||
// window to the grace (5s of 30s). The remaining window is a process crash
|
||||
// exactly between the two statements — accepted and documented here.
|
||||
rows, err := tx.Query(ctx, `
|
||||
SELECT DISTINCT family_id FROM refresh_tokens
|
||||
WHERE expires_at < NOW()
|
||||
|
||||
Reference in New Issue
Block a user