Skip to content

Security model

zpack parses archives it did not write. This page states plainly what that parser defends against and what it does not, so you can decide whether opening an untrusted .zpak is acceptable in your context.

In scope. Reading a .zpak supplied by someone else must not escape the destination directory, must not read or write outside the file, must not allocate unboundedly, and must not crash the process.

Out of scope. Confidentiality, authenticity, and integrity against a deliberate attacker. zpack does not encrypt, sign, or authenticate anything.

Extraction is confined by validating every path when the index is read, before any byte is written. Rejected: absolute paths, .. segments, empty segments, \, NUL, :, and non-UTF-8. See validation.

Because the check runs at open, a malicious path cannot be discovered halfway through an extraction that has already written files.

A colon is rejected anywhere in a path, not just in a C:/... position. On Windows, cfg:bak.txt names an alternate data stream on the file cfg - the bytes would land somewhere unpack never reports, and a round trip would silently lose the file. Rejecting the colon on every platform also keeps an archive’s behaviour identical across hosts.

Two independent bounds:

  1. At open. size is rejected if it exceeds what stored_size bytes could inflate to at deflate’s theoretical maximum ratio of 1032:1. Since stored_size is already bounded by the file’s real size, this bounds size by reality rather than by what the index claims.
  2. At read. Decompression stops at size + 1 bytes. A stream that keeps producing past what the index promised is reported as a length mismatch, never written out and never buffered.

A 1 KB archive therefore cannot cause a multi-gigabyte allocation or write.

entry_count is checked against the file size before the entry array is allocated. A header claiming four billion entries in a 40-byte file is rejected at the header, not after a four-billion-element allocation.

The .zpackignore parser caps at 1024 rules for the same reason: it bounds how much work matching a single path can cost.

offset + stored_size is checked for overflow rather than allowed to wrap into a small in-bounds number. Size totals in writeManifest use checked addition and report CorruptArchive rather than trapping. The inflate-ratio multiplication saturates instead of wrapping, so an absurd stored_size yields a huge ceiling rather than a small one.

StoredReader stops at the entry’s last byte even though the file continues past it, so a deflate stream cannot run on into a neighbouring entry or into the index.

pack builds into a temporary file and moves it into place only once complete, so a failure never replaces a good archive with a truncated one.

Not defended Consequence
Authenticity An attacker who can modify an archive can recompute every hash. verify will pass.
Confidentiality Paths and contents are plaintext. list reads them without decompressing anything.
Tampering Contents can be replaced wholesale, hashes and all.
Resource exhaustion by size A genuinely large archive genuinely takes a long time to verify. The bounds stop disproportion, not magnitude - apply your own limits on archive size.
Overlapping entries Two entries may reference the same bytes. Each is in-bounds and hash-checked, so this is odd but not unsafe.
Filesystem-level races An archive replaced between open and read yields CorruptArchive or HashMismatch, not a silent bad read - but zpack does not lock the file.

Build ReleaseSafe, not ReleaseFast, for anything that opens untrusted archives. Release binaries do exactly this. The safety checks cost a few percent of throughput and turn a class of bugs into a clean panic rather than undefined behaviour.

Verify before trusting. zpack verify after a download catches a truncated or corrupted transfer. It does not catch a substituted archive.

Sign out of band. Publish a detached signature or a checksum over a channel the archive does not travel on, and check it before opening. Releases publish a SHA256SUMS file for exactly this.

Extract to a scratch directory. Even with path confinement, extracting into a directory whose contents you care about means an archive can overwrite files in it. Extract somewhere empty and move what you want.

Open a security advisory rather than a public issue. A crash, a read or write outside the intended bounds, or an allocation disproportionate to the input all count.