Skip to main content

Hardened ZIP extraction for Python - secure by default.

Project description

SafeZip Logo

Hardened ZIP extraction for Python - secure by default.

PyPI Version Supported Python versions Build Status Documentation Status llms.txt - documentation for LLMs MIT Coverage

safezip is a zero-dependency, production-grade wrapper around Python’s zipfile module that defends against the most common ZIP-based attacks: ZipSlip path traversal, ZIP bombs, and malformed/crafted archives.

Features

  • ZipSlip protection - relative traversal, absolute paths, Windows UNC paths, Unicode lookalike attacks, and null bytes in filenames are all blocked.

  • ZIP bomb protection - per-member and cumulative decompression ratio limits abort extraction before runaway decompression can exhaust disk or memory.

  • File size limits - per-member size is checked against the declared header value at open time (Guard phase) and again against actual decompressed bytes during streaming (Streamer phase). Total extraction size is enforced cumulatively across all members at stream time.

  • ZIP64 consistency checks - crafted archives with inconsistent ZIP64 extra fields are rejected before decompression begins.

  • Symlink policy - configurable: REJECT (default), IGNORE, or RESOLVE_INTERNAL (symlink entries are extracted as regular files; no OS symlink is created on disk).

  • Atomic writes - every member is written to a temporary file first; the destination is only created after all checks pass. No partial files are left on disk after a security abort.

  • Secure by default - all limits are active without any configuration.

  • Zero dependencies - standard library only.

  • Environment variable overrides - all limits (including symlink_policy) can be set via SAFEZIP_* environment variables for containerised deployments.

Prerequisites

Python 3.10 or later. No additional packages required.

Installation

With uv:

uv pip install safezip

Or with pip:

pip install safezip

Quick start

Drop-in replacement for the common zipfile extraction pattern:

from safezip import safe_extract

safe_extract("path/to/file.zip", "/var/files/extracted/")

Or use the SafeZipFile context manager for more control:

from safezip import SafeZipFile

with SafeZipFile("path/to/file.zip") as zf:
    print(zf.namelist())
    zf.extractall("/var/files/extracted/")

Custom limits

from safezip import SafeZipFile

with SafeZipFile(
    "path/to/file.zip",
    max_file_size=100 * 1024 * 1024,   # 100 MiB per member
    max_total_size=500 * 1024 * 1024,  # 500 MiB total
    max_files=1_000,
    max_per_member_ratio=50.0,
    max_total_ratio=50.0,
) as zf:
    zf.extractall("/var/files/extracted/")

Recursive extraction

When an archive contains nested .zip files, set recursive=True to descend into them automatically. All safety limits apply at every level. Each nested archive is extracted into a directory named after it (without the extension). The .zip file itself is never written to disk.

from safezip import SafeZipFile

# archive.zip
#   readme.txt
#   data.zip          ← will be descended into, not extracted as a blob
#     report.csv

with SafeZipFile("path/to/archive.zip", recursive=True, max_nesting_depth=3) as zf:
    zf.extractall("/var/files/extracted/")

# Result on disk:
#   /var/files/extracted/readme.txt
#   /var/files/extracted/data/report.csv

With max_nesting_depth=0, opening any nested archive raises NestingDepthError before extracting a single byte from it:

import pytest
from safezip import SafeZipFile, NestingDepthError

# archive.zip
#   readme.txt
#   data.zip          ← depth 1 exceeds max_nesting_depth=0 → NestingDepthError
#     report.csv

with pytest.raises(NestingDepthError):
    with SafeZipFile(
        "path/to/archive.zip", recursive=True, max_nesting_depth=0
    ) as zf:
        zf.extractall("/var/files/extracted/")

Security event monitoring

from safezip import SafeZipFile, SecurityEvent

def my_monitor(event: SecurityEvent) -> None:
    print(f"[safezip] {event.event_type} archive={event.archive_hash}")

with SafeZipFile("path/to/file.zip", on_security_event=my_monitor) as zf:
    zf.extractall("/var/files/extracted/")

Environment variable overrides

All limits can be overridden without changing code:

export SAFEZIP_MAX_FILE_SIZE=104857600    # 100 MiB
export SAFEZIP_MAX_TOTAL_SIZE=524288000   # 500 MiB
export SAFEZIP_MAX_FILES=1000
export SAFEZIP_MAX_PER_MEMBER_RATIO=50
export SAFEZIP_MAX_TOTAL_RATIO=50
export SAFEZIP_MAX_NESTING_DEPTH=3
export SAFEZIP_SYMLINK_POLICY=reject      # reject | ignore | resolve_internal

Default limits

Parameter

Default

max_file_size

1 GiB

max_total_size

5 GiB

max_files

10 000

max_per_member_ratio

200

max_total_ratio

200

max_nesting_depth

3

symlink_policy

REJECT

recursive

False

Testing

All tests run inside Docker to prevent accidental pollution of the host system:

make test

To test a specific Python version:

make test-env ENV=py312

Writing documentation

Keep the following hierarchy:

=====
title
=====

header
======

sub-header
----------

sub-sub-header
~~~~~~~~~~~~~~

sub-sub-sub-header
^^^^^^^^^^^^^^^^^^

sub-sub-sub-sub-header
++++++++++++++++++++++

sub-sub-sub-sub-sub-header
**************************

License

MIT

Support

For security issues contact me at the e-mail given in the Author section.

For overall issues, go to GitHub.

Author

Artur Barseghyan <artur.barseghyan@gmail.com>

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

safezip-0.1.4.tar.gz (32.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

safezip-0.1.4-py3-none-any.whl (35.1 kB view details)

Uploaded Python 3

File details

Details for the file safezip-0.1.4.tar.gz.

File metadata

  • Download URL: safezip-0.1.4.tar.gz
  • Upload date:
  • Size: 32.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.11

File hashes

Hashes for safezip-0.1.4.tar.gz
Algorithm Hash digest
SHA256 9f8d42e36c1be5cf0abe8464f5147401c7150eb847961dfbd88df457371b1cc7
MD5 7be709b91c2fffbce2d0edbf7da6af49
BLAKE2b-256 ef2d8eb04f2fcadbd7c3fc2a6d7b7a7bb883d840471cfc683e4c13f3934fe036

See more details on using hashes here.

File details

Details for the file safezip-0.1.4-py3-none-any.whl.

File metadata

  • Download URL: safezip-0.1.4-py3-none-any.whl
  • Upload date:
  • Size: 35.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.11

File hashes

Hashes for safezip-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 24d24739bc8a3cbbc8c6771bd6dbbadf4ffeafd0e4c8045b56ad27b26c332299
MD5 7b05ffed8405837dbaf8ab4838e0a56f
BLAKE2b-256 909ee04571f7d373919848cde6c542a03c3c570949227fa107b19a21a149ad96

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page