CVE-2026-54567 Overview
Flask-Reuploaded, a file upload extension for the Flask web framework, contains an extension-denylist bypass in versions 1.5.0 through 1.5.x. The UploadSet.save(storage, name=...) function in src/flask_uploads/flask_uploads.py applies lowercase_ext to the default upload path but preserves case for caller-supplied name overrides. The extension_allowed check then evaluates an AllExcept denylist against a mixed-case extension. Attackers who control the name parameter can supply a mixed-case dangerous extension to bypass a lowercase denylist and write files to the served upload directory. The issue is fixed in version 1.6.0.
Critical Impact
On upload directories configured to execute scripts, stored files can run with the web server's privileges, compromising confidentiality, integrity, and availability.
Affected Products
- Flask-Reuploaded 1.5.0
- Flask-Reuploaded versions after 1.5.0 and prior to 1.6.0
- Flask applications using AllExcept denylist configurations with user-influenced name overrides
Discovery Timeline
- 2026-09-14 - CVE CVE-2026-54567 published to NVD
- 2026-09-16 - Last updated in NVD database
Technical Details for CVE-2026-54567
Vulnerability Analysis
The vulnerability is a case-folding asymmetry classified under [CWE-178] (Improper Handling of Case Sensitivity). Flask-Reuploaded normalizes extensions to lowercase along the default filename code path but skips this normalization when a caller passes an explicit name override to UploadSet.save(). The extension_allowed function then compares the case-preserved extension against a denylist that assumes lowercase input. An attacker submitting shell.PHP or payload.PhTml can slip past a denylist that only blocks php or phtml. The file is then written to the served upload directory using the original casing.
Root Cause
The root cause is inconsistent application of the lowercase_ext() helper. The default write path invokes it before validation, while the name override branch uses a case-preserving extension helper. Because extension_allowed performs string comparison against a normalized denylist, any capital letter in the attacker-controlled extension defeats the check. Pure allowlists remain safe because unrecognized casings fail the inclusion test rather than passing a denylist.
Attack Vector
Exploitation requires three preconditions: a denylist (AllExcept) configuration, a user-influenced name parameter passed to UploadSet.save(), and a deployment where the web server or interpreter resolves file extensions case-insensitively. Common Apache and IIS configurations satisfy the third condition. Path containment is not bypassed, so the file lands inside the configured upload directory. If that directory is execution-capable, the uploaded script runs under the web server's user context on the next request.
// Security patch note from CHANGES.rst
1.6.0 (unreleased)
------------------
- **SECURITY FIX**: Ensure overridden file names are normalized via
``lowercase_ext()`` prior to extension validation checking and writing to
disk.
Source: GitHub Commit 5ded760
Detection Methods for CVE-2026-54567
Indicators of Compromise
- Files in Flask upload directories with mixed-case executable extensions such as .PHP, .PhTml, .AsPx, .JsP, or .Py
- Web server access logs showing requests to newly created upload paths shortly after multipart form POSTs
- Outbound network connections originating from the web server user immediately after upload activity
- Unexpected child processes (shell, interpreter) spawned by the Flask application worker
Detection Strategies
- Enumerate stored upload directories and flag any filename whose extension contains uppercase characters against a denylist policy
- Instrument UploadSet.save() calls to log the resolved final filename and the source of the name argument
- Correlate multipart upload requests with subsequent GET requests to the upload directory that trigger interpreter execution
- Review web server module configuration for case-insensitive handler mappings that expand the exploitable surface
Monitoring Recommendations
- Alert on process creation events where the parent is a Python or WSGI worker and the child is a shell or scripting interpreter
- Monitor filesystem writes into static/uploads/ or equivalent served directories for extensions matching an executable denylist under any case
- Track version metadata of flask-reuploaded across build pipelines to identify environments still running 1.5.x
How to Mitigate CVE-2026-54567
Immediate Actions Required
- Upgrade flask-reuploaded to version 1.6.0 or later across all environments
- Audit application code for UploadSet.save(storage, name=...) call sites where the name value derives from user input
- Convert AllExcept denylist configurations to explicit allowlists, which remain protected even on vulnerable versions
- Ensure upload directories are not configured as executable by the web server or interpreter
Patch Information
The fix is available in Flask-Reuploaded 1.6.0. The maintainer's commit normalizes overridden filenames through lowercase_ext() prior to extension validation and disk write. Details are documented in GitHub Security Advisory GHSA-937x-gpqr-72gg and Pull Request #186.
Workarounds
- Replace AllExcept denylists with allowlist-based UploadSet configurations that enumerate permitted extensions
- Strip or normalize user-supplied name arguments to lowercase before invoking UploadSet.save()
- Configure the web server to disable script execution within upload directories using explicit handler removals
- Serve uploaded content from a separate origin or storage bucket that cannot execute code
# Pin the fixed version in requirements.txt
flask-reuploaded>=1.6.0
# Example Apache configuration to disable execution in the upload directory
# <Directory "/var/www/app/static/uploads">
# RemoveHandler .php .phtml .phar
# RemoveType .php .phtml .phar
# php_flag engine off
# </Directory>
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.