secure_filename仅清理非法字符和路径分隔符,不校验后缀、不处理空字节与Unicode归一化,必须在其后手动白名单校验后缀、清洗空字节、过滤前导点,并结合内容检测与Web服务器规则才能保障安全。

Flask上传时怎么用 secure_filename 防止路径遍历
直接用 secure_filename 不等于安全,它只清理文件名里的非法字符和路径分隔符,但不校验后缀、不检查空字节、不处理 Unicode 归一化问题。比如 test.php%00.jpg 经过 secure_filename 可能变成 test.php.jpg,而某些旧版 Web 服务器仍会按 .php 解析。
实操建议:
- 必须在调用
secure_filename后,再手动提取扩展名并白名单校验,不能只信它返回的字符串 - 用
os.path.splitext(filename)[1].lower()获取真实后缀,避免依赖secure_filename的分割逻辑 - 对上传的原始
filename做一次filename.encode('utf-8').replace(b'\x00', b'')清洗,防空字节绕过 - 禁用点号开头的文件名(如
.htaccess),secure_filename默认保留前导点,需额外过滤
如何正确做后缀白名单校验(不是黑名单)
黑名单(比如禁止 .php、.py)永远漏判,攻击者换种写法就绕过;白名单才是唯一靠谱做法。但要注意:后缀 ≠ MIME 类型,不能靠 request.files['file'].mimetype 判断,浏览器可伪造。
实操建议:
- 只允许你明确需要的后缀,例如
{'.jpg', '.jpeg', '.png', '.pdf'},全部转小写比对 - 用
pathlib.Path(filename).suffix.lower()提取后缀,比str.split('.')[-1]更可靠(防a.b.c.png类多点名) - 如果业务真要支持
.tar.gz,别用.suffix,改用.suffixes并检查末尾两个后缀是否都在白名单里 - 校验必须放在
secure_filename之后、保存到磁盘之前,顺序错了就白做了
为什么不能只靠 werkzeug.utils.secure_filename 就完事
secure_filename 的设计目标只是“让文件名能在文件系统里安全存储”,不是“让文件内容安全执行”。它不管内容是不是恶意脚本,也不拦住同名不同扩展的绕过(比如 shell.jpeg.php 被重命名为 shell.jpeg.php —— 它根本没删掉第二个点)。
常见错误现象:
- 用户上传
webshell.jpg.php,secure_filename返回原样,后端没再校验后缀,直接保存为webshell.jpg.php - 上传
../../../etc/passwd,secure_filename变成etc_passwd,看似安全,但若后续代码拼接路径时又加了/uploads/,就可能意外暴露目录结构 - 上传带 BOM 或 ZWJ 的文件名(如
script\u200b.php),secure_filename不处理这些 Unicode 控制字符,部分服务器仍会识别为 PHP
生产环境还要补哪几道坎
光文件名和后缀控制远远不够。真实部署中,这几个点经常被跳过,结果上线就被打穿。
实操建议:
- 保存路径必须用
os.path.join(UPLOAD_FOLDER, secure_name),且UPLOAD_FOLDER设为绝对路径,避免相对路径拼接风险 - 上传后立即用
mimetypes.guess_type+python-magic(非file命令)做内容检测,校验实际文件类型是否匹配后缀 - 设置 Nginx/Apache 规则,禁止访问上传目录下的所有可执行后缀(如
/.php$、/.py$),哪怕文件名合法也不许执行 - 上传目录不要放在 Flask 静态目录下,更不要设为 Web 可写 + 可执行混合权限
最麻烦的其实是那个「先重命名再校验」的顺序错位,还有把 secure_filename 当万能盾牌的心态——它只是个工具,不是安检门。

















