# Flask-Reuploaded: Extension-denylist bypass via case-folding asymmetry in name-override path (incomplete-fix variant of CVE-2026-27641)

Flask-Reuploaded's name-override path re-validates extensions case-preservingly, so `shell.PHP` bypasses the lowercase denylist and lands executable files in the upload dir.

- Payout: Research
- Severity: high
- Program: Flask-Reuploaded
- Found by: hackkim
- Disclosed: 2026-07-17
- CVE: CVE-2026-54567
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-937x-gpqr-72gg
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-937x-gpqr-72gg
- Tags: area:web domain:web impact:integrity impact:rce kind:research platform:ghsa sev:high tech:python technique:incomplete-fix technique:source-review vuln:file-upload vuln:misconfiguration vuln:rce

**Target:** UploadSet.save() name-override extension validation in flask_uploads
**Root cause:** The v1.5.0 re-validation for the `name` override calls the case-preserving `extension()` helper instead of the case-normalizing `lowercase_ext()` used by the default path, so mixed-case extensions never match lowercase denylist tokens.
**Steps:**
1. Install `Flask-Reuploaded==1.5.0`; configure `UploadSet("filesdeny", AllExcept(SCRIPTS))` with a dest dir.
2. Confirm default path blocks: `files_deny.save(FileStorage(..., filename="shell.PHP"))` raises `UploadNotAllowed`.
3. Bypass via override: `files_deny.save(FileStorage(..., filename="upload.bin"), name="shell.PHP")` saves `shell.PHP` to disk.
4. Vary case for generality: `name="evil.pHp"` also saves; realpath containment inside dest still holds.
5. Negative controls: allowlist `IMAGES` with `name="shell.PHP"` denies; `name="photo.jpg"` saves.
**How found:** Noticed the v1.5.0 fix routed the new name-override re-validation through the case-preserving `extension()` while the default path uses `lowercase_ext()`; hypothesized the two paths disagree on case and tested mixed-case extensions against a denylist UploadSet.
**Exploitation:** Attacker with an authenticated upload endpoint that forwards a user-influenced `name` to `save()` uploads a PHP payload under a mixed-case extension; on case-insensitive execution surfaces (Windows/macOS FS, Apache AddHandler/AddType) the file executes under the web server's privileges.
**Escalation:** Dangerous-upload primitive escalates to RCE where the upload directory is served and extensions resolve case-insensitively.
**Insight:** When a patch adds a second validation path, diff it against the canonical normalizer the original path uses — hand-rolled checks that skip `lowercase_ext()`/lowercasing reintroduce the bug for denylist policies.

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

**Name-override save calls with mixed-case PHP**

```
[2] save(storage("upload.bin"), name="shell.PHP")           -> saved "shell.PHP", on_disk=True   (BYPASS)
[2b] save(storage("upload.bin"), name="evil.pHp")           -> saved "evil.pHp",  on_disk=True; containment intact
```

