GHSA-f5p7-2c9q-8896: phpMyFAQ has Stored XSS in FAQ Question/Answer via Encode-Decode Bypass of removeAttributes() Sanitization
SummaryThe 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:`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:`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`):**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****Expected response:** `200 OK` with the new FAQ ID.**Step 2: Verify XSS execution**Navigate to the public FAQ page (e.g., `https://target/content/1/{faqId}/en/harmless-faq-title.html`). The `<script>` tag in the answer body executes, sending the visitor's cookies to the attacker's server.Impact**Session hijacking:** An attacker with FAQ creation privileges can steal session cookies from any user (including administrators) who views the FAQ, enabling full account takeover.**Phishing:** The injected script can modify page content to display fake login forms or redirect users to malicious sites.**Worm propagation:** If the attacker captures an admin session, they can create additional malicious FAQs automatically, spreading the attack.**Scope:** Every unauthenticated visitor who views the compromised FAQ is affected. The XSS also fires in search results via `search.twig`.Recommended FixReplace the encode→decode→removeAttributes chain with a proper HTML sanitizer that operates on the DOM level. Use a library like [HTML Purifier](http://htmlpurifier.org/) or Symfony's [HtmlSanitizer](https://symfony.com/doc/current/html_sanitizer.html) component.**Immediate fix — add tag-level filtering to `removeAttributes()`** (`Filter.php`):**Recommended long-term fix:** Replace custom sanitization with Symfony's HtmlSanitizer, which is already a project dependency ecosystem:
CVSS v3.1: 5.4