GitHub Security Advisory: GHSA-9pq7-mfwh-xx2j
Package: thorsten/phpmyfaq (COMPOSER)
Severity: CRITICAL (CVSS 9.1)
CWE: CWE-307: Improper Restriction of Excessive Authentication Attempts
Vulnerable versions: <= 4.1.1
Patched in: 4.1.2

Summary: phpMyFAQ enables unauthenticated 2FA brute-force attack via /admin/check acceptance of arbitrary user-id

Details:
## 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 use