自动化交付

客户上传文件的名字,不能直接当存储路径

上传文件叫 `../../report.pdf`,不意味着服务器应该照这个名字保存。文件名来自用户,属于输入数据;磁盘路径属于系统权限边界。把二者混为一谈,容易出现路径穿越、重名覆盖以及 Windows 与 Linux 行为不同的问题。

阅读文本 / Reading text
本文使用合成样本演示处理方法。代码已通过隔离环境中的示例检查;这不代表对所有真实文件或产品效果的保证。处理重要文件前,请保留原件,并按文中的边界与验收项复核。 上传文件叫 ../../report.pdf ,不意味着服务器应该照这个名字保存。文件名来自用户,属于输入数据;磁盘路径属于系统权限边界。把二者混为一谈,容易出现路径穿越、重名覆盖以及 Windows 与 Linux 行为不同的问题。 显示名和存储名分开 保留原始名称用于界面展示,但由服务器生成内部标识。存储后缀也应来自服务端允许列表,而不是照抄用户给出的扩展名。下面只构造路径,不创建目录或文件。 最小路径构造器 from pathlib import Path import re def storage_path(root, internal_id, extension): if not re.fullmatch(r"[a-f0-9]{32}", internal_id): raise ValueError("invalid internal id") if extension not in {".pdf", ".docx", ".udoc"}: raise ValueError("unsupported extension") base = Path(root).resolve() candidate = (base / (internal_id + extension)).resolve() if candidate.parent != base: raise ValueError("outside storage root") return candidate root = Path.cwd() / "synthetic-uploads" identifier = "a" * 32 p = storage_path(root, identifier, ".pdf") assert p.parent == root.resolve() assert p.name == identifier + ".pdf" for bad in ["../../report", "A" * 32, "a" * 31, "a" * 32 + "/x"]: try: storage_path(root, bad, ".pdf") except ValueError: pass else: raise AssertionError(bad) 这个例子假定内部 ID 已由服务端生成。它不是面向任意磁盘攻击环境的安全写文件实现:检查与实际打开之间仍可能发生竞争,父目录权限和符号链接策略必须另行控制。唯一 ID 也不等于可以忽略“禁止覆盖”策略。 文件名清理不是文件鉴定 输入现象 应在哪层处理 不足之处 含路径分隔符 不用作存储路径 仅去掉斜线不够 扩展名为 PDF 内容探测与解析 扩展名可伪造 显示名含 HTML 界面输出转义 不能靠改磁盘名解决 相同显示名 内部 ID 隔离 不应覆盖前一份上传 对象存储也有同样的边界 R2 或其他对象存储没有传统目录穿越的全部语义,但对象键依然影响隔离、覆盖和公开 URL。不要把租户提供的字符串直接拼成共享前缀;下载端也不能仅凭键名推断授权。 文件进入翻译前就应有身份 接入 OmniDoc(产品入口) 工作流时,可以向用户继续展示熟悉的文件名,在内部使用独立任务 ID 与文件 ID。清楚的文件身份让失败重试、译稿下载和后续审校更容易对应,也减少把别人的同名文件当成当前结果的风险。 参考: Python pathlib 。 - 1 -
更新于 2026-10-08