bigywater.tech
Flask · 125

路径穿越与文件下载

用户给一个文件名,服务端怎么安全地把文件发出去?send_from_directory 保证了什么、没保证什么?文件名清洗会有什么副作用?这篇只验证标准库函数的行为(用一个目录外的哨兵文件确认没有泄漏)。

404
文件名指向基目录之外时 send_from_directory 的结果
200
基目录里指向目录外的符号链接被放行的状态码
报告.txt → txt
secure_filename 对中文文件名的处理

send_from_directory

实测 · 本机执行输出
  合法文件 a.txt                             → 200 'hello a'
  子目录里的 sub/b.txt                        → 200 'hello b'
  不存在的文件                                 → 404 '<!doctype html>\n<html lang=en>'
  先进再出 sub/../a.txt                      → 200 'hello a'
  跳到目录外 ../outside.txt                   → 404 '<!doctype html>\n<html lang=en>'
  嵌在中间 sub/../../outside.txt             → 404 '<!doctype html>\n<html lang=en>'
  绝对路径形式                                 → 308 '<!doctype html>\n<html lang=en>'
  含空字节                                   → 404 '<!doctype html>\n<html lang=en>'
  目录本身 sub                               → 404 '<!doctype html>\n<html lang=en>'
  目录外的哨兵内容有没有出现在任何响应里: False

上一篇:SQL Injection 与参数化查询;下一篇:文件上传类型校验。

本篇是防御向的验证:没有演示任何绕过方法,只确认标准函数会拒绝什么、会放行什么。

safe_join

实测 · 本机执行输出
  safe_join(基目录, 'a.txt'                   ) → <tmp>/pub/a.txt
  safe_join(基目录, 'sub/b.txt'               ) → <tmp>/pub/sub/b.txt
  safe_join(基目录, 'sub/../a.txt'            ) → <tmp>/pub/a.txt
  safe_join(基目录, '../outside.txt'          ) → None
  safe_join(基目录, '/outside.txt'            ) → None
  safe_join(基目录, 'sub/../../outside.txt'   ) → None
  safe_join(基目录, ''                        ) → <tmp>/pub

符号链接

实测 · 本机执行输出
  访问 pub/link.txt(链接到目录外)                → 200 'SENTINEL-OUTSIDE'
  自己再检查 realpath 是否仍在基目录内: False (False 就该拒绝)

下载响应头

实测 · 本机执行输出
  Content-Disposition: attachment; filename=" .txt"; filename*=UTF-8''%E4%B8%8B%E8%BD%BD%20%E7%9A%84%E6%96%87%E4%BB%B6.txt
  Content-Type: text/plain; charset=utf-8 | ETag: True | Last-Modified: True | Cache-Control: no-cache | Accept-Ranges: bytes
  Range: bytes=0-4 → 206 'hello' | Content-Range: bytes 0-4/7
  If-None-Match → 304
  中文名的文件 /files/报告.txt → 200 中文文件

secure_filename

实测 · 本机执行输出
  secure_filename('report.pdf'        ) → 'report.pdf'
  secure_filename('my file (1).txt'   ) → 'my_file_1.txt'
  secure_filename('../x.txt'          ) → 'x.txt'
  secure_filename('a/b/c.txt'         ) → 'a_b_c.txt'
  secure_filename('报告.txt'            ) → 'txt'
  secure_filename('.hidden'           ) → 'hidden'
  secure_filename(''                  ) → ''
  secure_filename('CON.txt'           ) → 'CON.txt'
  secure_filename('a.tar.gz'          ) → 'a.tar.gz'

分步演示

路径穿越与文件下载
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。Werkzeug 3.1.9
    • 没有验证:Windows 上的保留文件名与反斜杠路径、send_file 直接接收用户路径的用法、反向代理的 X-Accel-Redirect 与 X-Sendfile 方式、大文件的流式下载与内存、文件名里的换行与响应头注入(Werkzeug 对 download_name 的处理未单独验证)
    京ICP备2026067502号-1京公网安备11010502063487号