VYPR
Medium severity6.8NVD Advisory· Published May 26, 2026· Updated May 26, 2026

CVE-2026-44707

CVE-2026-44707

Description

Chatwoot is a customer engagement suite. From 2.14.0 to before 4.13.0, a Pre-Account Takeover (Pre-ATO) vulnerability existed in Chatwoot's authentication flow. Because email confirmation was not enforced before an account became usable, an attacker could pre-register an email address they did not own and set a password. If the legitimate owner of that email later signed in to Chatwoot using Google OAuth (or another OmniAuth provider), the OAuth flow silently confirmed the existing account without invalidating the attacker's pre-set credentials. The attacker could then continue to log in with the password they had originally chosen and access any data the victim subsequently entered into the dashboard, including PII, API keys, and other sensitive information. This vulnerability is fixed in 4.13.0.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Chatwoot 2.14.0 to before 4.13.0 allowed a pre-account takeover via OAuth: an attacker could register an unused email and still login after the real owner signed up via OAuth.

Vulnerability

Chatwoot versions 2.14.0 through 4.12.x (fixed in 4.13.0) did not enforce email confirmation before an account became usable. This allowed an attacker to pre-register any email address they did not own and set a password of their choice. The vulnerable code path resides in the OAuth callback controller (OmniAuthController), where skip_confirmation! was called before verifying whether the user had already been confirmed.

Exploitation

An attacker requires no prior authentication or special network position. The attacker pre-registers an email address (e.g., using a form on the Chatwoot instance) and sets a password. The legitimate owner of that email then signs in for the first time using Google OAuth (or another OmniAuth provider). The OAuth flow silently confirms the existing unconfirmed account without invalidating the attacker's pre-set password. The attacker can subsequently log in with the originally chosen password at any time.

Impact

Once the victim begins using the dashboard, the attacker—who still holds valid credentials—can log in and access all data entered by the victim, including personally identifiable information (PII), API keys, and other sensitive customer data. This constitutes full compromise of the victim's Chatwoot account (confidentiality and integrity breach) at the same privilege level as the victim (agent or administrator).

Mitigation

The issue is patched in Chatwoot v4.13.0 (commit 211fb11 [2], PR #13878 [1]). The fix captures oauth_user_needs_password_reset? before confirmation and rotates the stored password to a random string if the user was unconfirmed. Users are strongly urged to upgrade to 4.13.0 or later. If immediate upgrade is not possible, workarounds include: disabling OAuth sign-in providers, auditing accounts where confirmed_at was set after email/password sign-up, and forcing a password reset for all users [3].

AI Insight generated on May 26, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • Chatwoot/Chatwootreferences2 versions
    (expand)+ 1 more
    • (no CPE)
    • (no CPE)range: >=2.14.0, <4.13.0

Patches

1
211fb1102dd2

chore: rotate oauth password if unconfirmed (#13878)

https://github.com/chatwoot/chatwootShivam MishraApr 2, 2026via body-scan-shorthand
2 files changed · +34 0
  • app/controllers/devise_overrides/omniauth_callbacks_controller.rb+18 0 modified
    @@ -10,7 +10,12 @@ def omniauth_success
       private
     
       def sign_in_user
    +    # Capture before skip_confirmation! sets confirmed_at, which would
    +    # make oauth_user_needs_password_reset? return false and skip the
    +    # password reset for persisted unconfirmed users.
    +    needs_password_reset = oauth_user_needs_password_reset?
         @resource.skip_confirmation! if confirmable_enabled?
    +    set_random_password_if_oauth_user if needs_password_reset
     
         # once the resource is found and verified
         # we can just send them to the login page again with the SSO params
    @@ -20,7 +25,10 @@ def sign_in_user
       end
     
       def sign_in_user_on_mobile
    +    # See comment in sign_in_user for why this is captured before skip_confirmation!
    +    needs_password_reset = oauth_user_needs_password_reset?
         @resource.skip_confirmation! if confirmable_enabled?
    +    set_random_password_if_oauth_user if needs_password_reset
     
         # once the resource is found and verified
         # we can just send them to the login page again with the SSO params
    @@ -37,6 +45,7 @@ def sign_up_user
         return redirect_to login_page_url(error: 'business-account-only') unless validate_signup_email_is_business_domain?
     
         create_account_for_user
    +    set_random_password_if_oauth_user
         token = @resource.send(:set_reset_password_token)
         frontend_url = ENV.fetch('FRONTEND_URL', nil)
         redirect_to "#{frontend_url}/app/auth/password/edit?config=default&reset_password_token=#{token}"
    @@ -81,6 +90,15 @@ def create_account_for_user
         Avatar::AvatarFromUrlJob.perform_later(@resource, auth_hash['info']['image'])
       end
     
    +  def oauth_user_needs_password_reset?
    +    @resource.present? && (@resource.new_record? || !@resource.confirmed?)
    +  end
    +
    +  def set_random_password_if_oauth_user
    +    # Password must satisfy secure_password requirements (uppercase, lowercase, number, special char)
    +    @resource.update(password: "#{SecureRandom.hex(16)}aA1!") if @resource.persisted?
    +  end
    +
       def default_devise_mapping
         'user'
       end
    
  • spec/controllers/devise/omniauth_callbacks_controller_spec.rb+16 0 modified
    @@ -164,5 +164,21 @@ def set_omniauth_config(for_email = 'test@example.com')
             expect(response).to have_http_status(:ok)
           end
         end
    +
    +    it 'resets password for an unconfirmed persisted user on OAuth login' do
    +      with_modified_env FRONTEND_URL: 'http://www.example.com' do
    +        user = create(:user, email: 'unconfirmed-oauth@example.com', skip_confirmation: false)
    +        original_password_digest = user.encrypted_password
    +        set_omniauth_config('unconfirmed-oauth@example.com')
    +
    +        get '/omniauth/google_oauth2/callback'
    +        expect(response).to redirect_to('http://www.example.com/auth/google_oauth2/callback')
    +        follow_redirect!
    +
    +        user.reload
    +        expect(user).to be_confirmed
    +        expect(user.encrypted_password).not_to eq(original_password_digest)
    +      end
    +    end
       end
     end
    

Vulnerability mechanics

Root cause

"Missing password rotation on OAuth confirmation allows an attacker who pre-registered an unowned email to retain access after the legitimate owner authenticates."

Attack vector

An attacker pre-registers an email address they do not own, setting a password of their choice. Because email confirmation is not enforced before the account becomes usable, the account is created in an unconfirmed state. When the legitimate owner of that email later signs in via Google OAuth (or another OmniAuth provider), the OAuth flow silently confirms the account but, prior to the fix, did not rotate the attacker's password. The attacker retains the ability to log in with the originally chosen password and can access any data the victim subsequently enters into the Chatwoot dashboard, including PII, API keys, and other sensitive information [ref_id=1].

Affected code

The vulnerability resides in the OmniAuth callback controller at `app/controllers/devise_overrides/omniauth_callbacks_controller.rb`. The `sign_in_user` and `sign_in_user_on_mobile` methods called `skip_confirmation!` before checking whether the user was unconfirmed, meaning an attacker's pre-set password was never invalidated when the legitimate email owner later authenticated via OAuth [patch_id=2566841].

What the fix does

The patch adds two helper methods: `oauth_user_needs_password_reset?` (which returns true if the user is new or unconfirmed) and `set_random_password_if_oauth_user` (which replaces the stored password with a secure random string). Crucially, the password-reset check is captured *before* `skip_confirmation!` runs, because confirmation would set `confirmed_at` and mask the unconfirmed condition. This logic is applied in `sign_in_user`, `sign_in_user_on_mobile`, and the `sign_up_user` path. Users who lose password-based access can recover via the standard "Forgot password" flow, which is low-friction since they have already proven email ownership through OAuth [patch_id=2566841][ref_id=1].

Preconditions

  • inputThe attacker must pre-register an email address they do not own and set a password before the legitimate owner signs up.
  • authThe legitimate email owner must later sign in using Google OAuth or another OmniAuth provider.
  • configThe Chatwoot instance must have email confirmation enabled (confirmable) but not enforced before account use.

Generated on May 26, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.