GitHub Security Advisory: GHSA-whqh-9pq5-c7r3
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.1
Patched in: 4.1.2

Summary: phpMyFAQ has a SVG Sanitizer Entity Decoding Depth Limit Bypass Leading to Stored XSS

Details:
## Summary

The `SvgSanitizer::decodeAllEntities()` method limits recursive entity decoding to 5 iterations. By wrapping each character of `javascript` in an `href` attribute value with 5 levels of `&amp;` encoding around numeric HTML entities (e.g., `&amp;amp;amp;amp;amp;#106;` for `j`), an attacker can bypass both `isSafe()` detection and `sanitize()` removal. The uploaded SVG is served from the application origin with `image/svg+xml` content type, and the browser's XML parser fully decodes the remaining `&#NNN;` entities, resulting in a clickable `javascript:` link that executes arbitrary JavaScript.

## Details

**Root cause:** `decodeAllEntities()` at `phpmyfaq/src/phpMyFAQ/Helper/SvgSanitizer.php:223-249` limits entity decoding to `maxIterations=5`. Each iteration: (1) decodes `&#NNN;` numeric entities, (2) decodes `&#xHH;` hex entities, (3) calls `html_entity_decode()` which resolves one level of `&amp;` → `&`. With 5 levels of `&amp;` wrapping, all 5 iterations are consumed unwinding the `&amp;` nesting, leaving the final `&#NNN;` numeric entities unresolved.

**Code path:**

1. Authenticated user with `FAQ_EDIT` permission uploads SVG via `POST /admin/api/content/images` (`ImageController::upload()` at line 39)
2. File extension is `svg` → `SvgSanitizer::isSafe()` called (line 114)
3. `isSafe()` calls `decodeAllEntities()` — 5 iterations resolve `&amp;` nesting but leave `&#106;&#97;...` (numeric entities for `javascript`)
4. Pattern matching at line 47 (`/href\s*=\s*["\'][\s]*javascript\s*:/i`) does **not** match `&#106;&#97;...`
5. `isSafe()` returns **true** — file saved **without any sanitization**
6. SVG served directly by web server from `content/user/images/` with `image/svg+xml` MIME type
7. Browser's XML parser decodes `&#106;` → `j`, `&#97;` → `a`, etc., reconstructing `javascript:alert(document.domain)`
8. User clicks the SVG link → JavaScript executes in the phpMyFAQ origin

The bypass is even simpler than initially described — no `<script>` decoy tag is needed. Since `isSafe()` itself is bypassed, the file is stored without sanitization and the `sanitize()` code path is never reached.

**Relevant code in `decodeAllEntities()`:**

```php
// phpmyfaq/src/phpMyFAQ/Helper/SvgSanitizer.php:223-249
private function decodeAllEntities(string $content): string
{
    $previous = '';
    $decoded = $content;
    $maxIterations = 5;  // <-- insufficient for 5 levels of &amp; + numeric entity

    while ($decoded !== $previous && $maxIterations-- > 0) {
        $previous = $decoded;
        // Step 1: Decode decimal entities (&#106; → j)
        $decoded = preg_replace_callback('/&#(\d+);/', ...);
        // Step 2: Decode hex entities (&#x6A; → j)
        $decoded = preg_replace_callback('/&#x([0-9a-fA-F]+);/', ...);
        // Step 3: Decode named HTML entities (&amp; → &)
        $decoded = html_entity_decode($decoded, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    }
    // After 5 iterations with 5 &amp; levels: &#106; remains undecoded
    r