
Advisory: CVE-2026-38360 path traversal (CWE-22) in dash-uploader (Python/PyPI)
Unauthenticated path traversal in fohrloop/dash-uploader (Python, PyPI) allowing arbitrary file write, leading to (but not limited to) Remote Code Execution (RCE), application source code overwrite, stored XSS, and persistent backdoor installation.
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-38360 (NVD) |
| Vulnerability | Path Traversal (CWE-22) |
| CVSS 3.1 | 9.8 / Critical (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/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-38361 (DoS in same library) |
Three user-controlled parameters from request.form.get() in dash_uploader/httprequesthandler.py are passed directly to os.path.join() and os.makedirs() without any sanitization or validation:
upload_id (line 57 → line 161, BaseHttpRequestHandler.get_temp_root): controls the destination directory. An attacker can send upload_id=../../../../usr/local/lib/python3.10/site-packages and files are written to Python's package directory.
resumableFilename (line 51 → line 108, BaseHttpRequestHandler._post): controls the final filename. An attacker can traverse out of the upload directory via the filename even with a legitimate upload_id.
resumableIdentifier (line 54 → line 64, BaseHttpRequestHandler._post): used with os.makedirs() to create the temp directory. An attacker can create arbitrary directories anywhere on the filesystem.
The upload endpoint (/API/dash-uploader by default) requires no authentication. The http_request_handler hook added in v0.5.0 allows pre-request checks via post_before(), but the vulnerable _post() method reads all parameters directly from request.form after the hook returns. The hook cannot sanitize parameters before the library processes them. A developer who adds authentication via the hook is still vulnerable to path traversal from an authenticated user.
# dash_uploader/httprequesthandler.py
def _post(self):
resumableFilename = request.form.get("resumableFilename", default="error", type=str)
resumableIdentifier = request.form.get("resumableIdentifier", default="error", type=str)
upload_id = request.form.get("upload_id", default="", type=str)
...
temp_root = self.get_temp_root(upload_id) # upload_id flows in here
temp_dir = os.path.join(temp_root, resumableIdentifier) # raw user input -> os.path.join
if not os.path.isdir(temp_dir):
os.makedirs(temp_dir) # raw user input -> os.makedirs
def get_temp_root(self, upload_id):
return os.path.join(self.upload_folder, upload_id) # no sanitization: ../../ escapes upload_folder
Three independent traversal sinks share the same root cause: form values reach os.path.join and os.makedirs with no validation.
An unauthenticated remote attacker sends an HTTP POST multipart request to the upload endpoint. By injecting path traversal sequences (../) into the upload_id form parameter, the attacker controls the destination directory for the uploaded file. For example, upload_id=../../../../usr/local/lib/python3.10/site-packages writes files to Python's package directory, enabling RCE on the next interpreter startup via .pth auto-execution.
No authentication, session token, or CSRF token is required. Two additional parameters (resumableFilename and resumableIdentifier) provide independent traversal vectors through the same endpoint. The default library configuration as shown in the official quickstart documentation is exploitable with a single curl command.
Arbitrary file write to any directory writable by the server process. This translates to Remote Code Execution (RCE) through several well-known primitives:
RCE primitives
.pth file dropped into site-packages. Executes attacker-supplied code on the next interpreter startup.sitecustomize.py or usercustomize.py injection. Executes on every Python startup.app.wsgi, wsgi.py). Executes on next worker reload./etc/cron.d/, /etc/cron.hourly/, user crontab spool) when the process has the necessary privileges. Scheduled execution./etc/systemd/system/, ~/.config/systemd/user/). Executes on next service start or reboot./etc/ld.so.preload injection when the process runs as root. Preloads attacker code into every subsequent binary execution.~/.bashrc, ~/.profile, ~/.bash_profile). Executes on next interactive login of the app user.~/.ssh/authorized_keys append. Grants persistent SSH access to the host as the app user.Web-tier impact
site-packages), affecting every user on every page load until the application is restartedFilesystem impact
os.makedirs() with the unsanitized resumableIdentifier parameter (usable for inode exhaustion or for staging writes into nonexistent directory trees)dash_uploader/httprequesthandler.pyBaseHttpRequestHandler.get_temp_root()BaseHttpRequestHandler._post()Options for currently-deployed users, in order of preference:
dcc.Upload, the official upload component shipped with Plotly Dash. Files arrive at the callback as a base64 string; no filesystem-writing handler is exposed and no client-controlled destination path exists, so the bug class here does not apply. Best suited to small and medium files. For very large uploads, see item 2.werkzeug.utils.secure_filename() and a hardcoded server-side destination directory. Never accept client-supplied upload_id, filename, or identifier values as path components.upload_id, resumableFilename, and resumableIdentifier against a strict allowlist (e.g., UUIDs only) at a layer that rewrites or rejects the request before the library handler sees it. The library's http_request_handler hook does NOT prevent the traversal because parameters are read from request.form after the hook returns; sanitization must happen above the library..., encoded variants (%2e%2e, ..%2f, %2e%2e%2f), or absolute paths.| Date | Event |
|---|---|
| 2026-03-17 | Vulnerability discovered during security research on a production deployment. |
| 2026-03-19 | CVE request submitted to MITRE. |
| 2026-05-07 | CVE-2026-38360 assigned by MITRE. |
| 2026-05-07 | Public advisory published. |
| 2026-05-09 | CVE record published on the MITRE CVE database and the NVD. |
0.6.1 (stable line). Pre-releases extend to 0.7.0a2.dash. Optional dependency: pyyaml. License: MIT.Muhammad Fitri Bin Mohd Sultan