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.
Threat model
Section titled “Threat model”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.
Defences
Section titled “Defences”Path traversal
Section titled “Path traversal”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.
Windows alternate data streams
Section titled “Windows alternate data streams”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.
Decompression bombs
Section titled “Decompression bombs”Two independent bounds:
- At
open.sizeis rejected if it exceeds whatstored_sizebytes could inflate to at deflate’s theoretical maximum ratio of 1032:1. Sincestored_sizeis already bounded by the file’s real size, this boundssizeby reality rather than by what the index claims. - At read. Decompression stops at
size + 1bytes. 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.
Unbounded allocation
Section titled “Unbounded allocation”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.
Integer overflow
Section titled “Integer overflow”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.
Reads confined to the entry
Section titled “Reads confined to the entry”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.
Half-written output
Section titled “Half-written output”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.
Non-defences
Section titled “Non-defences”| 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. |
Recommendations
Section titled “Recommendations”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.
Reporting a vulnerability
Section titled “Reporting a vulnerability”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.