
Advisory: CVE-2026-38361 multiple DoS vulnerabilities (CWE-400/CWE-670) in dash-uploader (Python/PyPI)
Multiple unauthenticated Denial of Service (DoS) issues in fohrloop/dash-uploader (Python, PyPI), including (but not limited to) Out-of-Memory (OOM) process crash, file truncation to zero bytes, permanent disk exhaustion, and complete bypass of the documented max_file_size limit. Additional resource-abuse paths exist through the same unsanitized parameter set.
The repository was archived on 2025-07-19 with no active maintainer. Every published version (0.1.0 through 0.7.0a2) is affected and will remain so. The package still pulls roughly 28,000 monthly downloads.
Anyone running dash-uploader in production must apply a mitigation themselves. The recommended fix is to migrate to Plotly Dash's built-in dcc.Upload component. See Mitigation for full options.
| CVE ID | CVE-2026-38361 (NVD) |
| Vulnerability | Uncontrolled Resource Consumption (CWE-400), Always-Incorrect Control Flow Implementation (CWE-670) |
| CVSS 3.1 | 7.5 / High (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H) |
| Product | dash-uploader |
| Affected versions | 0.1.0 through 0.7.0a2 (all 18 releases) |
| Fixed version | none (project archived 2025-07-19) |
| Attack vector | Remote, unauthenticated |
| Discoverer | Muhammad Fitri Bin Mohd Sultan |
| Assigned by | MITRE, 2026-05-07 |
| Related | CVE-2026-38360 (path traversal in same library) |
The dash-uploader HTTP handler accepts unauthenticated POST requests with attacker-controlled parameters that flow into memory allocation, file operations, and directory creation with no bounds checks, no rate limits, and no cleanup mechanism. Four independent issues in the same code path:
Verified on a 7.7 GB system: 5 concurrent POST requests with resumableTotalChunks=30000000 triggered the Linux OOM killer within 2 seconds. Kernel log confirms:
Out of memory: Killed process 24203 (python3) total-vm:8302276kB, anon-rss:7068012kB
Each request allocates approximately 2.9 GB via a list comprehension over range(1, resumableTotalChunks + 1). The server process is terminated and the application becomes completely unavailable until manual restart.
A file containing 42 bytes of data was reduced to 0 bytes via a single POST request with resumableTotalChunks=0. The root cause is Python's all() returning True for empty iterables, which tricks the upload handler into treating zero chunks as a completed upload. The existing file is deleted via os.unlink() and replaced with an empty file.
10 orphaned temp directories with chunk files were created and persisted indefinitely on disk. A codebase-wide search across all source files for cleanup, ttl, expire, garbage, purge, cron, schedule, and periodic returned zero results. The only cleanup call (shutil.rmtree) executes exclusively on completed uploads. There is no mechanism to reclaim disk space from incomplete sessions.
max_file_size bypass (verified)The server accepted a 5 MB chunk for a file claiming resumableTotalSize=999999999999 (~999 GB) with HTTP 200. The max_file_size parameter is passed only to the React JavaScript component. The server never checks file size, chunk size, Content-Length, or Flask MAX_CONTENT_LENGTH. A developer setting max_file_size=10 has no server-side protection.
# dash_uploader/httprequesthandler.py
def _post(self):
resumableTotalChunks = request.form.get("resumableTotalChunks", type=int) # attacker-controlled, no bounds
...
chunk_paths = [
os.path.join(temp_dir, get_chunk_name(resumableFilename, x))
for x in range(1, resumableTotalChunks + 1) # unbounded; e.g. 30M -> ~2.9 GB -> OOM
]
upload_complete = all([os.path.exists(p) for p in chunk_paths]) # all([]) is True -> truncation when chunks=0
if upload_complete:
target_file_name = os.path.join(temp_root, resumableFilename)
if os.path.exists(target_file_name):
os.unlink(target_file_name) # existing file deleted
with open(target_file_name, "ab") as target_file:
for p in chunk_paths: # empty list -> empty file written
...
Same code path produces both the OOM (large resumableTotalChunks) and the file-truncation primitive (resumableTotalChunks=0).
An attacker sends unauthenticated POST requests to the /API/resumable endpoint.
resumableTotalChunks=30000000 allocate ~2.9 GB each, triggering the OOM killer.resumableTotalChunks=0; Python all([])=True tricks the server into overwriting the target file with empty content.No authentication or privileges required.
resumableTotalChunks)os.makedirs() with unsanitized resumableIdentifierall() returning True for empty iterables when resumableTotalChunks=0max_file_size is enforced only in client-side JavaScript while the server-side handler performs zero size validation and never sets Flask MAX_CONTENT_LENGTH