Security vulnerabilities

  • Status Closed
  • Assigned To
    cbay
  • Private
Attached to Project: Security vulnerabilities
Opened by Bores - 07.08.2026
Last edited by cbay - 07.08.2026

FS#433 - Password Reset Tokens Not Invalidated After Password Change, Enabling Persistent Account Takeover

The admin panel password reset at admin.alwaysdata.com issues tokens with a 3-day validity window. When a user triggers multiple resets, using one token to change the password does not invalidate the others. An older sibling token remains fully functional and can overwrite the new password at any point within its 3-day lifetime, giving an attacker persistent account takeover that the victim cannot revoke.

Vulnerable endpoint: https://admin.alwaysdata.com/user/reset_password/ Token generation: https://admin.alwaysdata.com/password/lost/

ROOT CAUSE

Token validation does not include the current password hash. Django's default PasswordResetTokenGenerator binds tokens to the password hash, so any credential change voids all outstanding tokens. The custom implementation here validates only user_id, timestamp, and expiration. A consumed token correctly shows "invalid" on revisit (per-token single-use works), but unconsumed sibling tokens remain valid after a password change through a different token.

REPRODUCTION

Tested 2026-08-07, Chrome on Windows 11, production admin.alwaysdata.com. One test account owned by me.

1. Triggered two password resets for the same account within 6 seconds. Received Token A (timestamp 1786058809) and Token B (timestamp 1786058815).

2. Opened Token B. Reset form displayed. Set password to "TokenBProof_2026!" and submitted. Server returned 302 to /login/ (password changed).

 [Screenshot 1: Token B form with password visible]
 [Screenshot 2: redirect to /login/]

3. Opened Token A (issued before the password change). Reset form still displayed. Set password to "TokenAProof_ATO!" and submitted. Server returned 302 to /login/.

 [Screenshot 3: Token A form still active after password was already changed]
 [Screenshot 4: redirect to /login/]

4. On the login page, entered email + "TokenAProof_ATO!" and submitted. Server showed 2FA prompt ("You have enabled two-factor authentication, so please enter your security code"), confirming the stale token's password is now the active credential.

 [Screenshot 5: login form with credentials]
 [Screenshot 6: 2FA prompt]

Expected: Token A should show "invalid link" after the password was changed via Token B.
Actual: Token A remains functional and overwrites the new password.

IMPACT

An attacker who gains temporary access to a victim's email (phishing, shared workstation, corporate mail breach) can save one reset link. Even if the victim notices and resets their own password, the attacker's saved link remains valid for up to 3 days. Using it overwrites whatever password the victim set, completing account takeover.

On alwaysdata, this exposes: web hosting management, SSH access, databases, mailboxes, domain/DNS configuration, API tokens, and billing.

All testing was performed against my own accounts only. A standalone PoC script (poc.py) is attached.

SUGGESTED FIX

Include the password hash in token validation by switching to Django's built-in PasswordResetTokenGenerator. Alternatively, store a per-user token nonce and increment it on every password change, rejecting tokens with stale nonce values. Reducing the token lifetime from 3 days to 1 hour would also limit the exploitation window.

Closed by  cbay
07.08.2026 07:20
Reason for closing:  Duplicate
Additional comments about closing:  

https://security.alwaysda ta.com/task/82

Loading...

Available keyboard shortcuts

Tasklist

Task Details

Task Editing