应使用os.path.realpath获取真实绝对路径后,用startswith()检查是否在授权目录内;需防范符号链接绕过,禁止用户创建软链或逐级校验父路径。

用 os.path.realpath 替代 os.path.basename 做路径归一化
很多人误以为 os.path.basename 能防路径穿越,实际它只是字符串截断,对 ../../../etc/passwd 返回 passwd,拼接后仍会打开真实系统文件。真正有效的是让 Python 解析出绝对路径再比对范围。
正确做法是:先用 os.path.realpath 展开所有 ..、. 和符号链接,再判断是否落在白名单目录内:
import os
<p>BASE_DIR = '/var/www/uploads'
user_path = request.GET.get('file', '')
full_path = os.path.join(BASE_DIR, user_path)
real_path = os.path.realpath(full_path)</p><p>if not real_path.startswith(os.path.abspath(BASE_DIR) + os.sep):
raise PermissionError("Path traversal attempt detected")</p><h1>✅ 此时 real_path 是真实、展开后的绝对路径,且已确认在 BASE_DIR 下</h1>
注意:os.path.realpath 会自动处理 URL 解码后的 %2e%2e%2f(即 ../),但前提是解码发生在你调用前——Django 默认已做,无需额外 decode。
禁止手动拼接路径 + open() 读取上传文件
Django 自带的 FileResponse、send_from_directory 等机制内部做了路径校验,但一旦你绕过它们,直接用 open(os.path.join(UPLOAD_ROOT, filename)),就等于把防护全交给了自己写错的逻辑。
常见错误场景包括:
- 用
request.GET['file']拼接静态资源路径,比如/static/ + user_input - 在自定义视图里调用
Path.read_text()或open()读取用户指定文件名 - 把上传后的文件路径存进数据库,后续再用该路径去
open()—— 若没重命名或没校验存储路径,风险仍在
修复原则:上传文件一律走 request.FILES,读取静态文件一律用 django.views.static.serve(并确保 document_root 是绝对路径且不包含用户输入)。
上传文件名必须重命名,且禁用原始 filename 的任何语义
攻击者常利用原始文件名中的路径字符(如 ../../shell.php)或编码(如 %2e%2e%2f)触发遍历。Django 的 secure_filename 并不存在——那是 Flask 的函数,Django 不提供等价物。
所以你必须自己剥离所有路径信息,并强制生成新名:
- 丢弃
request.FILES['file'].name的全部内容,只取扩展名(且需白名单校验) - 用
uuid.uuid4().hex或time.time_ns()生成前缀,拼上安全扩展名 - 绝不在重命名逻辑里拼接用户可控字符串,哪怕只是
user_id + '_' + original_name
示例:
import uuid
from pathlib import Path
<p>ext = Path(request.FILES['file'].name).suffix.lower()
if ext not in {'.jpg', '.png', '.pdf'}:
raise ValueError("Unsupported extension")</p><p>new_name = f"{uuid.uuid4().hex}{ext}"
file_path = Path(UPLOAD_ROOT) / new_name
request.FILES['file'].save(file_path)</p>
上传目录权限与 Web 服务器配置必须隔离执行权限
即使代码层堵住了所有路径穿越,如果上传目录被 Web 服务器(如 Nginx/Apache)配置成可执行 PHP/Python 脚本,攻击者上传 shell.jpg 后,只要服务端存在解析漏洞(如 Apache 的 AddHandler、Nginx 的 fastcgi_split_path_info 配置错误),照样能 RCE。
关键配置项:
- Nginx:对上传目录加
location ^~ /media/ { deny all; },或明确禁用脚本执行:location ~* \.(php|py|sh)$ { return 403; } - Apache:在上传目录的
.htaccess中写Options -ExecCGI -Indexes,并确保AllowOverride None防止被覆盖 - Linux 文件系统:上传目录权限设为
755,文件设为644,且属主不是 Web 进程用户(如不设为www-data)
这个环节最容易被忽略:代码写得再严,挡不住 Web 服务器把 .jpg 当 PHP 执行。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











