GHSA-VQV8-J3MJ-WJXJ

Vulnerability from github – Published: 2026-05-06 19:50 – Updated: 2026-05-06 19:50
VLAI
Summary
wger: trainer_login open redirect - ?next= parameter not validated against host
Details

Summary

The trainer_login view in wger redirects to request.GET['next'] directly via HttpResponseRedirect() without calling url_has_allowed_host_and_scheme(). After the trainer successfully enters impersonation mode, their browser is redirected to any attacker-controlled URL supplied in the ?next= parameter, enabling Referer exfiltration and phishing.

Details

File: wger/core/views/user.py, approximately line 203

# VULNERABLE - wger/core/views/user.py
if not own:
    request.session['trainer.identity'] = orig_user_pk
    if request.GET.get('next'):
        return HttpResponseRedirect(request.GET['next'])   # no host/scheme validation

After the impersonation logic succeeds, the view performs no validation of the next parameter before issuing the redirect. An attacker who can deliver a crafted link (e.g. /en/user/2/trainer-login?next=https://evil.example/steal) to a trainer can redirect the trainer's browser to any external host immediately after the impersonation session is established. The Location header contains the raw attacker-controlled URL.

Affected endpoint: - GET /en/user/<user_pk>/trainer-login -> wger.core.views.user.trainer_login (the ?next= redirect branch)

Suggested patch:

--- a/wger/core/views/user.py
+++ b/wger/core/views/user.py
+from django.utils.http import url_has_allowed_host_and_scheme
+
 if not own:
     request.session['trainer.identity'] = orig_user_pk
-    if request.GET.get('next'):
-        return HttpResponseRedirect(request.GET['next'])
+    next_url = request.GET.get('next')
+    if next_url and url_has_allowed_host_and_scheme(
+        next_url, allowed_hosts={request.get_host()}, require_https=request.is_secure()
+    ):
+        return HttpResponseRedirect(next_url)
     return HttpResponseRedirect(reverse('core:index'))

Adding @require_POST to trainer_login (see also VULN-030) moves the next parameter to the POST body where CSRF protection applies and eliminates the combined CSRF + open-redirect attack surface entirely.

PoC

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

Step 1 - Authenticate as trainer:

POST /en/user/login HTTP/1.1
Host: target
Content-Type: application/x-www-form-urlencoded

username=trainer1&password=[REDACTED]&csrfmiddlewaretoken=[REDACTED]

-> 302 Found; Set-Cookie: sessionid=[trainer1_session]

Step 2 - Trainer clicks (or is delivered) the crafted link:

GET /en/user/2/trainer-login?next=https://evil.example/steal HTTP/1.1
Host: target
Cookie: sessionid=[trainer1_session]

-> 302 Found
   Location: https://evil.example/steal

Step 3 - Attacker server logs Referer:

Referer: http://target/en/user/2/trainer-login?next=https://evil.example/steal
(victim user_pk and next URL exposed)

Reproducibility: 2/2 runs.

Impact

An attacker who can deliver a crafted URL to a trainer (phishing email, malicious gym management system integration, social engineering) can redirect the trainer's browser to an attacker-controlled domain after the trainer enters impersonation mode. The redirect leaks:

  • The wger URL structure (including the impersonated user's user_pk) via the browser Referer header.
  • The session-rebound cookie (if the attacker page subsequently triggers an authenticated request with credentials: 'include' targeting wger, any same-site cookie without SameSite=Strict is attached).

Combined with the trainer-login scope bypass (submitted separately), this primitive allows an attacker to silently impersonate arbitrary gym=None users and then land the trainer on an attacker page for credential harvesting.

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), requires victim interaction (click), scope change (attacker's origin).

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2.5"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "wger"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.6"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-601"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-06T19:50:52Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "### Summary\n\nThe `trainer_login` view in wger redirects to `request.GET[\u0027next\u0027]` directly via `HttpResponseRedirect()` without calling `url_has_allowed_host_and_scheme()`. After the trainer successfully enters impersonation mode, their browser is redirected to any attacker-controlled URL supplied in the `?next=` parameter, enabling Referer exfiltration and phishing.\n\n### Details\n\n**File**: `wger/core/views/user.py`, approximately line 203\n\n```python\n# VULNERABLE - wger/core/views/user.py\nif not own:\n    request.session[\u0027trainer.identity\u0027] = orig_user_pk\n    if request.GET.get(\u0027next\u0027):\n        return HttpResponseRedirect(request.GET[\u0027next\u0027])   # no host/scheme validation\n```\n\nAfter the impersonation logic succeeds, the view performs no validation of the `next` parameter before issuing the redirect. An attacker who can deliver a crafted link (e.g. `/en/user/2/trainer-login?next=https://evil.example/steal`) to a trainer can redirect the trainer\u0027s browser to any external host immediately after the impersonation session is established. The `Location` header contains the raw attacker-controlled URL.\n\n**Affected endpoint**:\n- `GET /en/user/\u003cuser_pk\u003e/trainer-login` -\u003e `wger.core.views.user.trainer_login` (the `?next=` redirect branch)\n\n**Suggested patch**:\n\n```diff\n--- a/wger/core/views/user.py\n+++ b/wger/core/views/user.py\n+from django.utils.http import url_has_allowed_host_and_scheme\n+\n if not own:\n     request.session[\u0027trainer.identity\u0027] = orig_user_pk\n-    if request.GET.get(\u0027next\u0027):\n-        return HttpResponseRedirect(request.GET[\u0027next\u0027])\n+    next_url = request.GET.get(\u0027next\u0027)\n+    if next_url and url_has_allowed_host_and_scheme(\n+        next_url, allowed_hosts={request.get_host()}, require_https=request.is_secure()\n+    ):\n+        return HttpResponseRedirect(next_url)\n     return HttpResponseRedirect(reverse(\u0027core:index\u0027))\n```\n\nAdding `@require_POST` to `trainer_login` (see also VULN-030) moves the `next` parameter to the POST body where CSRF protection applies and eliminates the combined CSRF + open-redirect attack surface entirely.\n\n### PoC\n\nTested on `wger/server:latest` Docker image. Victim: `trainer1` (`gym.gym_trainer` permission).\n\nStep 1 - Authenticate as trainer:\n\n```\nPOST /en/user/login HTTP/1.1\nHost: target\nContent-Type: application/x-www-form-urlencoded\n\nusername=trainer1\u0026password=[REDACTED]\u0026csrfmiddlewaretoken=[REDACTED]\n\n-\u003e 302 Found; Set-Cookie: sessionid=[trainer1_session]\n```\n\nStep 2 - Trainer clicks (or is delivered) the crafted link:\n\n```\nGET /en/user/2/trainer-login?next=https://evil.example/steal HTTP/1.1\nHost: target\nCookie: sessionid=[trainer1_session]\n\n-\u003e 302 Found\n   Location: https://evil.example/steal\n```\n\nStep 3 - Attacker server logs Referer:\n\n```\nReferer: http://target/en/user/2/trainer-login?next=https://evil.example/steal\n(victim user_pk and next URL exposed)\n```\n\nReproducibility: 2/2 runs.\n\n### Impact\n\nAn attacker who can deliver a crafted URL to a trainer (phishing email, malicious gym management system integration, social engineering) can redirect the trainer\u0027s browser to an attacker-controlled domain after the trainer enters impersonation mode. The redirect leaks:\n\n- The wger URL structure (including the impersonated user\u0027s `user_pk`) via the browser `Referer` header.\n- The session-rebound cookie (if the attacker page subsequently triggers an authenticated request with `credentials: \u0027include\u0027` targeting wger, any same-site cookie without SameSite=Strict is attached).\n\nCombined with the trainer-login scope bypass (submitted separately), this primitive allows an attacker to silently impersonate arbitrary `gym=None` users and then land the trainer on an attacker page for credential harvesting.\n\n**Affected deployments**: every wger instance where `gym.gym_trainer` is delegated to non-admin users.\n\n**Severity**: Medium (CVSS 5.4). Network-reachable, low complexity, low privilege (trainer role), requires victim interaction (click), scope change (attacker\u0027s origin).",
  "id": "GHSA-vqv8-j3mj-wjxj",
  "modified": "2026-05-06T19:50:52Z",
  "published": "2026-05-06T19:50:52Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/wger-project/wger/security/advisories/GHSA-vqv8-j3mj-wjxj"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/wger-project/wger"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "wger: trainer_login open redirect - ?next= parameter not validated against host"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…