fix: patch Devise confirmable race condition vulnerability (#13843)

Devise 4.9.x has a race condition in the reconfirmable flow where
concurrent email change requests can desynchronize the confirmation
token from `unconfirmed_email`, letting an attacker confirm an email
they don't own. We use `:confirmable` with `reconfirmable = true`, so
we're directly exposed.

The upstream fix is in Devise 5.0.3, but we can't upgrade —
`devise-two-factor` only supports Devise 5 from v6.4.0, which also
raised its Rails minimum to 7.2+. No released version supports both
Devise 5 and Rails 7.1.

This PR ports the Devise 5.0.3 fix locally by overriding
`postpone_email_change_until_confirmation_and_regenerate_confirmation_token`
on the User model to persist the record before regenerating the token.
This is a stopgap — remove it once the dependency chain allows upgrading
to Devise 5.

### How to test

Sign in as a confirmed user and change your email. The app should send a
confirmation to the new address while keeping the current email
unchanged until confirmed.
This commit is contained in:
Shivam Mishra
2026-03-19 10:00:09 +05:30
committed by GitHub
parent 18dc77aa56
commit 284977687c
2 changed files with 20 additions and 0 deletions

View File

@@ -1,3 +1,4 @@
--- ---
ignore: ignore:
- CVE-2021-41098 # https://github.com/chatwoot/chatwoot/issues/3097 (update once azure blob storage is updated) - CVE-2021-41098 # https://github.com/chatwoot/chatwoot/issues/3097 (update once azure blob storage is updated)
- GHSA-57hq-95w6-v4fc # Devise confirmable race condition — patched locally in User model (remove once on Devise 5+)

View File

@@ -192,6 +192,25 @@ class User < ApplicationRecord
Chatwoot.mfa_enabled? Chatwoot.mfa_enabled?
end end
# Workaround for Devise 4.9.x race condition vulnerability (GHSA-57hq-95w6-v4fc).
#
# The Confirmable module's reconfirmable flow has a race condition where concurrent
# email change requests can desynchronize confirmation tokens, allowing an attacker
# to confirm an email they don't own. Fixed in Devise 5.0.3 by persisting
# unconfirmed_email before regenerating the confirmation token.
#
# We can't upgrade to Devise 5.0.3 because devise-two-factor only added Devise 5
# support in v6.4.0, which simultaneously raised its Rails minimum to 7.2+.
# No released version supports both Devise 5 and Rails 7.1.
#
# This override applies the same fix locally: force-mark unconfirmed_email
# as dirty before assignment so ActiveRecord always writes it, keeping it
# in sync with the regenerated confirmation token. Remove once on Devise 5+.
def postpone_email_change_until_confirmation_and_regenerate_confirmation_token
unconfirmed_email_will_change!
super
end
private private
def remove_macros def remove_macros