阅读文本 / 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