CVE-2026-62669 Overview
CVE-2026-62669 is an authentication bypass vulnerability in the Grav Login plugin, which provides login, basic access control, and session-wide messaging for the Grav flat-file content management system (CMS). The login.regenerate2FASecret task verifies only that a pending-session user exists rather than requiring $user->authorized. An attacker who already knows the victim's password can invoke taskRegenerate2FASecret() during the pending Time-based One-Time Password (TOTP) challenge, overwrite the twofa_secret, read the replacement from the response, and complete authentication without the victim's second factor. The issue is fixed in version 3.8.11. It is categorized as an Improper Authentication weakness [CWE-287].
Critical Impact
Attackers with a valid victim password can defeat two-factor authentication (2FA) on Grav sites and gain full account access.
Affected Products
- Grav Login Plugin versions prior to 3.8.11
- Grav CMS installations that enable 2FA via the Login plugin
- Any Grav deployment relying on the Login plugin for access control
Discovery Timeline
- 2026-08-19 - CVE-2026-62669 published to the National Vulnerability Database (NVD)
- 2026-08-19 - Last updated in NVD database
Technical Details for CVE-2026-62669
Vulnerability Analysis
Grav's Login plugin implements TOTP-based 2FA as a two-step flow. After a valid username and password submission, the session stores the user object but marks authorized = false until the correct TOTP code is entered. The taskRegenerate2FASecret() handler in classes/Controller.php is used from the profile page to regenerate a QR code for re-enrollment. The handler gated access on $user->exists() alone, which is true for any user with a valid pending session, including one still awaiting the 2FA challenge.
An attacker who has already submitted the victim's correct password can send a request to /task:login.regenerate2FASecret before completing the TOTP step. The server generates a new secret, persists it to the account, and returns it in the JSON response. The attacker then computes a valid TOTP from the returned secret and submits it, completing login as the victim.
Root Cause
The authorization check confused authentication with authorization. $user->exists() returns true whenever the session holds a user object, but Grav intentionally keeps authorized = false during the 2FA delay. The regenerate task also lacked Cross-Site Request Forgery (CSRF) nonce validation, allowing the state-changing action to be invoked without the session-bound login-form-nonce.
Attack Vector
Exploitation is network-based and requires prior knowledge of the victim's password, for example from credential stuffing, phishing, or a prior data breach. No user interaction is required from the victim.
/** @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: Grav Login Plugin commit 5d1b722. The patch tightens the gate to require authorized === true, blocking pending-2FA sessions from calling the task.
Detection Methods for CVE-2026-62669
Indicators of Compromise
- HTTP POST requests to /task:login.regenerate2FASecret (or the equivalent ?task=login.regenerate2FASecret) originating outside the authenticated profile-page workflow.
- Consecutive events showing a password submission followed within seconds by a regenerate2FASecret call and a successful TOTP submission from the same session or IP.
- Unexpected changes to a user's twofa_secret field in Grav account YAML files without a corresponding profile update.
Detection Strategies
- Ingest web server access logs into a SIEM or data lake and alert on regenerate2FASecret calls that are not preceded by a session in the authorized=true state.
- Correlate authentication events: flag any 2FA success where the secret used differs from the secret recorded prior to the login attempt.
- Monitor for POST requests to the task endpoint that lack the login-form-nonce parameter, which the 3.8.11 patch now requires.
Monitoring Recommendations
- Enable verbose Grav Login plugin logging and forward events to centralized logging.
- Track file modification times on user/accounts/*.yaml for unexpected twofa_secret rewrites.
- Alert on repeated failed logins followed by a successful login from the same source, indicating possible credential-stuffing chained with this bypass.
How to Mitigate CVE-2026-62669
Immediate Actions Required
- Upgrade the Grav Login plugin to version 3.8.11 or later immediately.
- Force password resets for privileged accounts and rotate all twofa_secret values after patching.
- Review authentication logs for any suspicious regenerate2FASecret activity since 2FA was enabled on the site.
Patch Information
The fix is delivered in Grav Login plugin 3.8.11. The relevant commit 5d1b722298cb947d8f434025d121b99152a2c630 adds && $user->authorized === true to the guard in classes/Controller.php and adds a CSRF nonce (login-form-nonce) to the client request in js/2fa.js. See the GitHub Security Advisory GHSA-7mgc-c7pq-3rr3 and the Grav Login Plugin 3.8.11 release.
Workarounds
- If patching is delayed, restrict access to /task:login.regenerate2FASecret at the web server or reverse proxy layer to authenticated admin sessions only.
- Temporarily disable 2FA re-enrollment via the plugin and require administrators to reset secrets through direct file edits.
- Enforce Web Application Firewall (WAF) rules that block requests to the task endpoint missing a valid login-form-nonce parameter.
# Update via Grav CLI
bin/gpm update login
# Verify installed plugin version is 3.8.11 or later
bin/gpm info login | grep -i version
# Example nginx rule to block the vulnerable endpoint without a nonce
# (remove after upgrading to 3.8.11)
location ~* /task[:=]login\.regenerate2FASecret {
if ($arg_login-form-nonce = "") { return 403; }
}
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

