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.
Vulnerable code path (FAQ create — FaqController.php):
At line 120, the answer content is filtered:
$content = Filter::filterVar($data->answer, FILTER_SANITIZE_SPECIAL_CHARS);
Filter::filterVar() calls filterSanitizeString() (Filter.php:135-144) which applies htmlspecialchars(), converting <script> to <script>. 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:
->setAnswer(Filter::removeAttributes(html_entity_decode(
(string) $content,
ENT_QUOTES | ENT_HTML5,
encoding: 'UTF-8',
)))
html_entity_decode() converts <script> 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):
<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...
4.1.24.1.2Exploitability
AV:NAC:LPR:LUI:RScope
S:CImpact
C:LI:LA:N5.4/CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N