CVE-2026-62671 Overview
The Grav Login plugin contains a Cross-Site Request Forgery (CSRF) vulnerability [CWE-352] in its login.regenerate2FASecret task. Versions prior to 3.8.11 accept a top-level GET request through the TaskServiceProvidertask: URI parameter without requiring a login-form nonce, Origin check, or Referer check. Under the default SameSite=Lax session cookie policy, an off-site navigation can invoke taskRegenerate2FASecret() in a logged-in victim's session. An attacker can overwrite the victim's Time-based One-Time Password (TOTP) secret and force two-factor re-enrollment. The issue is fixed in Grav Login plugin version 3.8.11.
Critical Impact
A remote attacker can trigger a CSRF navigation that overwrites an authenticated user's TOTP secret, disrupting two-factor authentication enrollment for logged-in Grav users.
Affected Products
- Grav CMS Login plugin versions prior to 3.8.11
- Deployments using the plugin's default SameSite=Lax session cookie policy
- Grav instances with two-factor authentication enabled for administrative or user accounts
Discovery Timeline
- 2026-08-19 - CVE-2026-62671 published to the National Vulnerability Database (NVD)
- 2026-08-19 - Last updated in NVD database
Technical Details for CVE-2026-62671
Vulnerability Analysis
The Grav Login plugin exposes the login.regenerate2FASecret task through the TaskServiceProvider, which routes requests using a task: URI parameter. The pre-3.8.11 handler accepts GET requests and does not validate a login-form nonce, the Origin header, or the Referer header. Under the default SameSite=Lax cookie policy, browsers include session cookies on top-level cross-site navigations. An attacker hosting a malicious page can therefore cause an authenticated victim's browser to invoke taskRegenerate2FASecret(), which calls TwoFactorAuth::createSecret() and overwrites the user's stored TOTP secret. The user's next authenticator prompt fails, and the account requires two-factor re-enrollment before normal use resumes.
Root Cause
The root cause is missing CSRF protection on a state-changing task. The controller gated the operation on $user->exists() alone, without verifying request authenticity or full session authorization. During a pending 2FA challenge, the session user is authenticated but not authorized, allowing the endpoint to mint and read a new secret.
Attack Vector
Exploitation requires the victim to be logged into a vulnerable Grav site and to visit or be redirected to an attacker-controlled page. That page issues a top-level navigation to the login.regenerate2FASecret task URL. The browser attaches the session cookie under SameSite=Lax, and the server processes the task without CSRF checks.
/** @var UserInterface $user */
$user = $this->grav['user'];
- if ($user->exists()) {
+ // Require a fully authorized session, not merely an existing one.
+ // During the 2FA challenge the session user is authenticated but
+ // NOT authorized (Login sets `authorized = false` while the login is
+ // delayed). Gating on `exists()` alone let a pending attacker mint
+ // and read the victim's new 2FA secret (GHSA-7mgc). Legitimate
+ // first-time enrollment / QR regeneration happens from the account
+ // profile page where the user is fully logged in (authorized=true),
+ // so that flow still passes this gate.
+ if ($user->exists() && $user->authorized === true) {
/** @var TwoFactorAuth $twoFa */
$twoFa = $this->grav['login']->twoFactorAuth();
$secret = $twoFa->createSecret();
Source: GitHub Login Plugin Commit 5d1b722. This patch in classes/Controller.php requires a fully authorized session before regenerating a 2FA secret.
Detection Methods for CVE-2026-62671
Indicators of Compromise
- Web server access logs containing GET requests to URLs matching /task:login.regenerate2FASecret or the equivalent ?task=login.regenerate2FASecret parameter
- Requests to the 2FA regeneration task with a Referer header pointing to an external, untrusted origin
- Unexpected updates to user account files where the twofa_secret field changes without a corresponding admin profile action
- User reports of TOTP authenticator codes suddenly failing after browsing external sites
Detection Strategies
- Monitor Grav application logs for login.regenerate2FASecret task invocations that lack a preceding authenticated profile page load
- Alert on any state-changing task request delivered through HTTP GET rather than POST with a nonce parameter
- Correlate 2FA secret changes with user session activity to identify regenerations that originate from cross-site navigations
Monitoring Recommendations
- Enable verbose access logging on the Grav admin endpoints and forward logs to a centralized analytics platform
- Track file modification events on the Grav user data directory, particularly changes to per-user YAML files that store twofa_secret
- Baseline normal 2FA enrollment frequency per user and alert on statistical outliers
How to Mitigate CVE-2026-62671
Immediate Actions Required
- Upgrade the Grav Login plugin to version 3.8.11 or later on all Grav instances
- Audit user accounts for unexpected TOTP secret changes and require re-enrollment for any affected users
- Review web server logs for historical requests to login.regenerate2FASecret originating from external referrers
Patch Information
The fix is available in Grav Login plugin release 3.8.11. The patch, tracked in commit 5d1b7222, adds a login-form-nonce parameter to the client-side js/2fa.js POST request and requires $user->authorized === true server-side before regenerating the secret. See the GitHub Security Advisory GHSA-4px8-7p53-282r for full advisory text.
Workarounds
- Restrict access to the Grav admin panel by IP allowlist at the web server or reverse proxy layer until the patch is applied
- Configure the web server to reject GET requests to login.regenerate2FASecret and require POST with a valid nonce
- Instruct administrative users to log out of Grav sessions before browsing untrusted sites if immediate patching is not possible
event.preventDefault();
let element = $(this);
let url = `${config.base_url_relative}/task${config.param_sep}login.regenerate2FASecret`;
+ let nonce = element.closest('.twofa-wrapper').find('input[name="login-form-nonce"]').val();
element.attr('disabled', 'disabled').find('> .fa').addClass('fa-spin');
- jQuery.post(url, function(response) {
+ jQuery.post(url, { 'login-form-nonce': nonce }, function(response) {
$('[data-2fa-image]').attr('src', response.image);
$('[data-2fa-secret]').text(response.secret);
$('[data-2fa-value]').val(response.secret);
Source: GitHub Login Plugin Commit 5d1b722. The js/2fa.js patch attaches the login-form-nonce token to the regeneration request so the server can validate request authenticity.
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

