os.path.basename过滤无效,因其仅字符串截断不解析路径语义,对../、url编码、空字节等完全无感,无法防御路径遍历。

直接说结论:文件上传漏洞不能靠单点过滤堵住,必须从文件名、内容、存储路径、执行环境四层同步拦截;漏掉任意一层,os.path.basename或ALLOWED_EXTENSIONS都等于摆设。
为什么用 os.path.basename 过滤文件名是无效的
它只做字符串截断,不解析路径语义。对用户传入的 ../../../etc/passwd,os.path.basename 返回 passwd,拼进 /var/www/uploads/ 后实际打开的仍是系统文件。更危险的是,它对 URL 编码(如 %2e%2e%2f)、空字节(\x00)、符号链接完全无感——Django 和 Flask 默认已解码 URL,攻击载荷在你拿到参数时早已还原。
常见错误写法:
filename = request.GET.get('file')
safe_name = os.path.basename(filename) # ❌
with open(f'/var/www/static/{safe_name}') as f:
return f.read()
真正该做的是:丢弃原始文件名,强制重命名;若必须保留语义,先 os.path.realpath 展开再比对根目录。
上传文件必须重命名且剥离所有路径信息
原始 request.FILES['file'].name 是不可信输入源,含 ../、%2e%2e%2f、shell.php.jpg 等构造极易绕过扩展名检查。Django 没有 secure_filename 函数(那是 Flask-WTF 的),你得自己处理。
- 生成新文件名:用
secrets.token_urlsafe(16)+ 固定后缀(如.png),不要拼接原始扩展名 - 禁止使用
os.path.splitext提取扩展名——攻击者可传shell.php%00.jpg绕过 - 若业务需保留原始类型,应从文件头(前 512 字节)用
python-magic或imghdr检测真实 MIME,而非信任content-type头 - 上传后立即校验:对图片类文件,额外扫描是否含
<?php、eval(、system(等字符串
存储路径必须物理隔离 + realpath 校验
上传目录绝不能放在 Web 根目录下(如 /var/www/html/uploads/),而应置于不可直连的位置(如 /var/www/uploads/),再通过视图代理访问。任何拼接用户输入路径的操作都必须禁用。
若业务必须支持子目录(如按用户 ID 分区),用映射表代替拼接:
user_id = request.POST.get('user_id')
safe_dir = {'1001': '/var/www/uploads/user_1001', '1002': '/var/www/uploads/user_1002'}.get(user_id)
if not safe_dir:
raise PermissionError("Invalid user_id")
读取文件时,必须走 os.path.realpath 归一化:
BASE_DIR = '/var/www/uploads'
full_path = os.path.join(BASE_DIR, user_supplied_name)
real_path = os.path.realpath(full_path)
if not real_path.startswith(os.path.abspath(BASE_DIR) + os.sep):
raise PermissionError("Path traversal attempt detected")
注意:os.path.realpath 会自动解析符号链接,但前提是你的服务账户不能有权限创建软链——上线前应禁用用户创建链接的能力。
Web 服务器层必须禁用上传目录的脚本解析
即使文件名和路径都安全,只要上传目录被 Web 服务器当作 PHP/Python 脚本执行,漏洞就依然成立。Nginx 和 Apache 都需显式配置:
- Nginx:在上传目录 location 块中加
location ~ \.(php|phtml|py|sh)$ { return 403; } - Apache:在
.htaccess或虚拟主机配置中加RemoveHandler .php .phtml .py或SetHandler none - 确保上传文件权限为
644,目录为755,且属主不是 Web 服务运行用户(如www-data) - 若需公开访问(如头像),用独立静态域名(
https://static.example.com/),并确保该域不加载任何动态模块
最易被忽略的是:上传后的文件路径若存进数据库,后续再用该路径去 open() 或 Path.read_text(),等同于重复引入风险——所有读取操作都必须重新走 realpath + starts_with 校验,不能信任存储值。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











