bigywater.tech
Flask · 38

secure_filename() 的安全处理

用户给的文件名能直接用吗?secure_filename 清洗了什么、没清洗什么?中文名为什么变成空?这篇用真实的写文件演示路径穿越,并用 2 万个随机输入验证清洗后是否安全。

0 / 20030
模糊测试里 secure_filename 之后越出上传目录的个数 / 输入总数
空串
中文名「中文」清洗后的结果
None
safe_join 遇到 ../x 时的返回

secure_filename 的结果

实测 · 本机执行输出
  输入                                   → 输出
  'report.pdf'                         → 'report.pdf'
  'my report 2024.pdf'                 → 'my_report_2024.pdf'
  '../../etc/passwd'                   → 'etc_passwd'
  '..\\..\\windows\\system32\\cmd.exe' → 'windowssystem32cmd.exe'
  '/absolute/path/x.txt'               → 'absolute_path_x.txt'
  '.htaccess'                          → 'htaccess'
  '..hidden'                           → 'hidden'
  'a/b/c.txt'                          → 'a_b_c.txt'
  '中文报告.docx'                          → 'docx'
  'résumé.pdf'                         → 'resume.pdf'
  'file name (1).txt'                  → 'file_name_1.txt'
  'a\x00b.txt'                         → 'ab.txt'
  'a\nb.txt'                           → 'a_b.txt'
  'CON'                                → 'CON'
  'con.txt'                            → 'con.txt'
  'NUL.txt'                            → 'NUL.txt'
  'AUX'                                → 'AUX'
  'COM1.txt'                           → 'COM1.txt'
  'LPT1'                               → 'LPT1'
  '   .txt  '                          → 'txt'
  'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx → 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'(长度 304)
  ''                                   → ''
  '...'                                → ''
  '.'                                  → ''
  'tar.gz'                             → 'tar.gz'
  'a.tar.gz'                           → 'a.tar.gz'
  '版本.v2.txt'                          → 'v2.txt'
  'emoji😀.png'                         → 'emoji.png'
  '$HOME/x'                            → 'HOME_x'
  'name;rm -rf.txt'                    → 'namerm_-rf.txt'

上一篇:上传文件大小限制;下一篇:Response 对象构造过程;上传类型校验见文件上传类型校验,路径穿越与下载见路径穿越与文件下载。

中文文件名

实测 · 本机执行输出
  secure_filename('中文报告.docx') → 'docx'
  secure_filename('中文') → ''
  secure_filename('你好.txt') → 'txt'

不清洗的后果

实测 · 本机执行输出
  filename='../escaped.txt' → 实际写入 <实验目录>/fltmp/f38/escaped.txt(是否在上传目录里面:False)
  filename='../../escaped2.txt' → 实际写入 <实验目录>/fltmp/escaped2.txt(是否在上传目录里面:False)
  上传目录之外多出的文件: ['escaped.txt']

模糊测试

实测 · 本机执行输出
  测试 20030 个输入(其中 339 个清洗后为空串),越出上传目录的有 0 个

白名单与随机文件名

实测 · 本机执行输出
  safe_name('photo.PNG'       ) → 'c6d3fbce362d.png'
  safe_name('a.exe'           ) → None
  safe_name('中文.jpg'          ) → None
  safe_name('noext'           ) → None
  safe_name('../x.pdf'        ) → 'abcd5a3723e4.pdf'
  safe_name('archive.tar.gz'  ) → None

safe_join

实测 · 本机执行输出
  safe_join(上传目录, 'a.txt'         ) → '<uploads>/a.txt'
  safe_join(上传目录, 'sub/a.txt'     ) → '<uploads>/sub/a.txt'
  safe_join(上传目录, '../x'          ) → None
  safe_join(上传目录, '/etc/passwd'   ) → None
  safe_join(上传目录, 'sub/../../x'   ) → None
  safe_join(上传目录, 'a/../a.txt'    ) → '<uploads>/a.txt'
  safe_join(上传目录, ''              ) → '<uploads>'

分步演示

secure_filename
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动。实验在 macOS 上运行,Windows 保留名的处理依赖 os.name,没有在 Windows 上验证
    • 没有验证:Windows 上 secure_filename 对 CON、NUL 等保留名的处理、符号链接导致的逃逸(上传目录里存在指向外部的链接)、文件系统大小写与 Unicode 规范化造成的同名冲突、磁盘配额
    京ICP备2026067502号-1京公网安备11010502063487号