- Status Closed
-
Assigned To
cbay - Private
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.
Loading...
Available keyboard shortcuts
- Alt + ⇧ Shift + l Login Dialog / Logout
- Alt + ⇧ Shift + a Add new task
- Alt + ⇧ Shift + m My searches
- Alt + ⇧ Shift + t focus taskid search
Tasklist
- o open selected task
- j move cursor down
- k move cursor up
Task Details
- n Next task
- p Previous task
- Alt + ⇧ Shift + e ↵ Enter Edit this task
- Alt + ⇧ Shift + w watch task
- Alt + ⇧ Shift + y Close Task
Task Editing
- Alt + ⇧ Shift + s save task
Screenshot 1.png
poc.py