fohrloop/dash-uploader(Python,PyPI)中存在多个未经认证的**拒绝服务(DoS)**问题,包括但不限于:内存溢出(OOM)导致进程崩溃、文件截断为零字节、永久性磁盘耗尽,以及完全绕过文档中声明的 max_file_size 限制。通过同一组未经清理的参数,还存在其他资源滥用路径。
该仓库已于 2025-07-19 归档,无活跃维护者。所有已发布版本(0.1.0 至 0.7.0a2)均受影响且将持续受影响。该包每月仍有约 28,000 次下载。
任何在生产环境中运行 dash-uploader 的人都必须自行应用缓解措施。推荐的修复方案是迁移到 Plotly Dash 内置的 dcc.Upload 组件。完整选项请参阅 缓解措施。
| CVE ID | CVE-2026-38361 (NVD) |
| 漏洞类型 | 无节制的资源消耗(CWE-400),始终不正确的控制流实现(CWE-670) |
| CVSS 3.1 | 7.5 / 高(AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H) |
| 产品 | dash-uploader |
| 受影响版本 | 0.1.0 至 0.7.0a2(所有 18 个版本) |
| 修复版本 | 无(项目于 2025-07-19 归档) |
| 攻击向量 | 远程,未经认证 |
| 发现者 | Muhammad Fitri Bin Mohd Sultan |
| 分配机构 | MITRE,2026-05-07 |
| 相关 | CVE-2026-38360(同一库中的路径遍历) |
dash-uploader 的 HTTP 处理程序接受未经认证的 POST 请求,其中带有攻击者控制的参数,这些参数直接流入内存分配、文件操作和目录创建,且没有任何边界检查、速率限制或清理机制。同一代码路径中存在四个独立的问题:
在 7.7 GB 系统上验证:5 个并发 POST 请求携带 resumableTotalChunks=30000000,在 2 秒内触发了 Linux OOM killer。内核日志确认:
Out of memory: Killed process 24203 (python3) total-vm:8302276kB, anon-rss:7068012kB
每个请求通过一个列表推导式 range(1, resumableTotalChunks + 1) 分配约 2.9 GB。服务器进程被终止,应用程序完全不可用,直到手动重启。
一个包含 42 字节数据的文件通过单个 POST 请求(resumableTotalChunks=0)缩小为 0 字节。根本原因是 Python 的 all() 对空可迭代对象返回 True,从而欺骗上传处理程序将零个块视为已完成的上传。现有文件通过 os.unlink() 删除,并替换为空文件。
创建了 10 个包含块文件的孤立临时目录,并永久保留在磁盘上。在整个源代码中搜索 cleanup、ttl、expire、garbage、purge、cron、schedule 和 periodic,结果为零。唯一的清理调用(shutil.rmtree)仅在完全上传后执行。没有任何机制可以回收未完成会话的磁盘空间。
max_file_size 绕过(已验证)服务器接受了一个声称 resumableTotalSize=999999999999(约 999 GB)的 5 MB 块,并返回 HTTP 200。max_file_size 参数仅传递给 React JavaScript 组件。服务器从未检查文件大小、块大小、Content-Length 或 Flask MAX_CONTENT_LENGTH。开发人员设置 max_file_size=10 没有任何服务器端保护。
# dash_uploader/httprequesthandler.py
def _post(self):
resumableTotalChunks = request.form.get("resumableTotalChunks", type=int) # 攻击者控制,无边界
...
chunk_paths = [
os.path.join(temp_dir, get_chunk_name(resumableFilename, x))
for x in range(1, resumableTotalChunks + 1) # 无界;例如 30M -> ~2.9 GB -> OOM
]
upload_complete = all([os.path.exists(p) for p in chunk_paths]) # all([]) 为 True -> 当 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) # 删除现有文件
with open(target_file_name, "ab") as target_file:
for p in chunk_paths: # 空列表 -> 写入空文件
...
同一代码路径同时产生 OOM(大的 resumableTotalChunks)和文件截断原语(resumableTotalChunks=0)。
攻击者向 /API/resumable 端点发送未经认证的 POST 请求。
resumableTotalChunks=30000000,每个分配约 2.9 GB,触发 OOM killer。resumableTotalChunks=0;Python all([])=True 欺骗服务器用空内容覆盖目标文件。无需认证或权限。
resumableTotalChunks)导致无界内存分配,触发 Linux OOM killer,导致服务器进程崩溃os.makedirs() 和未经清理的 resumableIdentifier 创建任意深度目录,导致文件系统 inode 耗尽resumableTotalChunks=0 时,Python all() 对空可迭代对象返回 True,导致文件截断为零字节,造成数据破坏max_file_size 仅在客户端 JavaScript 中执行,而服务器端处理程序不进行任何大小验证,也从未设置 Flask MAX_CONTENT_LENGTHdash_uploader/httprequesthandler.py(BaseHttpRequestHandler._post 方法)dash_uploader/upload.py(Upload 函数,max_file_size 参数)dash_uploader/configure_upload.py(缺少 MAX_CONTENT_LENGTH)对于当前已部署用户,按优先顺序排列的选项:
dcc.Upload,即 Plotly Dash 随附的官方上传组件。它没有块计数参数,没有磁盘上的临时状态,并且遵循 Flask MAX_CONTENT_LENGTH。这里提到的四个问题都不适用。最适合中小型文件。对于非常大的上传,请参见第 2 项。MAX_CONTENT_LENGTH),对客户端提供的块计数设置边界,并对接受的文件名设置白名单。MAX_CONTENT_LENGTH(该库没有),并在应用程序或反向代理层拒绝满足以下任一条件的输入:
resumableTotalChunks <= 0resumableTotalChunks 超过合理边界(例如 10,000)resumableTotalSize 超过开发人员配置的 max_file_size| 日期 | 事件 |
|---|---|
| 2026-03-19 | 在一次生产部署的安全研究期间发现漏洞。 |
| 2026-03-22 | 向 MITRE 提交 CVE 请求。 |
| 2026-05-07 | MITRE 分配 CVE-2026-38361。 |
| 2026-05-07 | 公开安全公告发布。 |
| 2026-05-09 | CVE 记录发布在 MITRE CVE 数据库 和 NVD 上。 |
0.6.1(稳定版)。预发布版本扩展至 0.7.0a2。dash。可选依赖:pyyaml。许可证:MIT。Muhammad Fitri Bin Mohd Sultan