Skip to content

[BUG] parseExcel/uploadExcel 用客户端文件名落盘,Windows 下可越出上传目录 (CWE-22) #1448

Closed

Description

@borealisalastor-byte

SQLBot Version
最新 main(核实于 a0b6145)

Run Mode
Docker 或源码运行均适用(问题在请求处理层)

Describe the bug

POST /datasource/parseExcel 和 POST /datasource/uploadExcel 直接把 multipart 的 file.filename 拼进落盘文件名,只按 / 做了一次切分,写入前也没有做 realpath / commonpath 归属校验:

# backend/apps/datasource/api/datasource.py  parse_excel()
ALLOWED_EXTENSIONS = {".xlsx", ".xls", ".csv"}
if not file.filename.lower().endswith(tuple(ALLOWED_EXTENSIONS)):
    raise HTTPException(400, trans('i18n_ds_unsupported_file_format'))

os.makedirs(path, exist_ok=True)
filename = f"{file.filename.split('.')[0].split('/')[-1]}_{hashlib.sha256(uuid.uuid4().bytes).hexdigest()[:10]}.{file.filename.split('.')[-1]}"
save_path = os.path.join(path, filename)
with open(save_path, "wb") as f:
    f.write(await file.read())
# backend/apps/datasource/api/datasource.py  upload_excel()  (标记 deprecated,但路由仍注册)
ALLOWED_EXTENSIONS = {"xlsx", "xls", "csv"}
if not file.filename.lower().endswith(tuple(ALLOWED_EXTENSIONS)):
    raise HTTPException(400, trans('i18n_ds_unsupported_file_format'))

filename = f"{file.filename.split('.')[0]}_{hashlib.sha256(uuid.uuid4().bytes).hexdigest()[:10]}.{file.filename.split('.')[1]}"
save_path = os.path.join(path, filename)
with open(save_path, "wb") as f:
    f.write(await file.read())

path 取自 settings.EXCEL_PATH(同文件第 39 行)。相比 apps/terminology/api/terminology.py:190、apps/data_training/api/data_training.py:179 已经采用的 os.path.basename(...) + os.path.commonpath(...) 写法,这两个入口漏了同样的处理。

To Reproduce

构造 multipart 的文件名(name 部分即 file.filename):

  1. ..\..\evil.xlsx —— split('/') 不认 Windows 分隔符,剥不掉目录部分;split('.')[0] 还会把 ..\.. 截成 ..\,最终文件名变成 ..\evil_<hash>.xlsx,在 Windows 主机上写到上传目录外。
  2. C:evil.xlsx —— Windows 驱动器相对路径,os.path.join(path, "C:evil_<hash>.xlsx") 会整体替换上传目录。
  3. evil.xlsx 之外的 a.txlsx —— uploadExcel 的扩展名白名单是不带点的 {"xlsx", "xls", "csv"},endswith 会放过非 Excel 名称。
  4. 不含点的名字(如 invoice)—— upload_excel 里的 file.filename.split('.')[1] 直接 IndexError,接口返回 500。

Expected behavior

  • 落盘文件名只取客户端名称的最后一个路径分量,/ 与 \ 都要当作分隔符;
  • 拒绝空名、.、..、含 :(驱动器相对路径 / NTFS 数据流)或 NUL 的名称;
  • 写入前用 os.path.realpath + os.path.commonpath 确认路径仍在 settings.EXCEL_PATH 内;
  • 扩展名白名单按带点的后缀匹配(与 parseExcel 现在的写法一致);
  • parseExcel 返回的 filePath 只包含文件名本身,不含任何目录分量。

Additional context

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions