CVE-2026-81637: Replayable OAuth2 CSRF state retained after a failed callback in AshAuthentication
Insufficient Session Expiration vulnerability in team-alembic AshAuthentication allows an attacker who obtains a victim's OAuth2 state value to replay the callback and sign that victim into an attacker-controlled account.
AshAuthentication.Strategy.OAuth2.Plug.callback/2 clears the stored sessionparams through a rebinding step inside its with chain, conn <- deletesession(conn, sessionkey). Elixir evaluates the else block in the scope enclosing the with, so every failure path (the provider returning ?error=accessdenied, an invalid code, a token-exchange error, or a registration or sign-in failure) reaches storeauthenticationresult/2 holding the original connection and the session entry is never removed. The value the module's own comment describes as protection against a CSRF-related attack is therefore consumed only when authentication succeeds, and survives a cancelled or failed attempt until the next request phase or session expiry.
This issue affects ashauthentication: from 0.6.0 before 4.15.0 and from 5.0.0-rc.0 before 5.0.0-rc.14.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
ash_authenticationto a version that resolves this vulnerability.Fixed in 4.15.0 - Upgrade
Upgrade
ash_authenticationto a version that resolves this vulnerability.Fixed in 5.0.0-rc.14
Event History
Frequently Asked Questions
Which deployments are affected?
Deployments using ash_authentication versions from 0.6.0 before 4.15.0, or from 5.0.0-rc.0 before 5.0.0-rc.14, are affected. The vulnerable behavior is in AshAuthentication.Strategy.OAuth2.Plug.callback/2.
What does an attacker need to exploit this issue?
An attacker needs to obtain a victim's OAuth2 state value and replay the OAuth2 callback after the victim has cancelled or otherwise failed an authentication attempt. Successful exploitation can sign the victim into an attacker-controlled account.
When does the OAuth2 state remain reusable?
The stored state is not removed on failed callback paths, including an access_denied response, an invalid code, token-exchange errors, and registration or sign-in failures. It is consumed when authentication succeeds, but otherwise remains until the next request phase or session expiry.
How can I determine whether an authentication attempt left a replayable state behind?
Review failed OAuth2 callback handling for the affected callback paths and check whether the session entry containing session_params remains after the failure. In affected versions, the failure handling reaches store_authentication_result/2 with the original connection rather than the connection returned by delete_session/2.