This page puts the other pages together: the steps for a reader, the steps for a writer, and the places where real archives and real tools disagree. A ZIP file is often untrusted input, and the format leaves room for an archive to mean different things to different readers.
A reader, step by step
- Find the end record. Search backwards through the last 65,557 bytes
for
50 4B 05 06, as described under finding the record. Take care of the comment ambiguity. - Check for ZIP64. If the 20 bytes before the end record are a locator, read the ZIP64 end record it points to. Replace any end record value holding a sentinel with the 64-bit value.
- Find the directory. Work out the
shift for any data in front of
the archive. Check that a
50 4B 01 02signature is where the end record says. - Read the directory. For each entry, up to the stated count, read the 46 fixed bytes and the name, extra field and comment. Apply the ZIP64 block. Decode the name with bit 11 or the Unicode blocks.
- Check each member before extracting it. Reject unsafe paths. Check that the version needed is one you can handle, and that the method is one you support.
- Find the data. Go to the local header offset (plus the shift).
Check the
50 4B 03 04signature, and read the local name and extra lengths. The data starts 30 + name length + extra length bytes in. Use the local lengths, not the central ones. - Decrypt and decompress. If bit 0 is set, use the encryption the headers name. Stop decompressing if the output would exceed the central directory’s uncompressed size or your limit.
- Verify. The output must be exactly the uncompressed size and have the CRC-32 stated, except for AE-2, which relies on its authentication code.
Treat the central directory as the true list of members. Fall back to walking the local headers only for a truncated archive with no usable directory, and report what you did. The explorer does this for files without an end record.
A writer, step by step
- Choose each member’s method, name and time. Use forward slashes in names and no leading slash. Set bit 11 when the name is not plain ASCII and is in UTF-8.
- Write the local header. If the output can seek, write placeholders and go back later. If not, set flag bit 3, write zeros for the CRC-32 and sizes, and use a method that is self-terminating, such as Deflate.
- Write the data as compressed and, if needed, encrypted, keeping the CRC-32 and sizes of the original.
- If the output is not seekable, write the data descriptor with its signature. Otherwise, go back and fill in the local header.
- When all members are written, note the offset, then write one central header per member, with the version made by, external attributes and local header offset.
- If any value will not fit, or a member uses it, write ZIP64 fields and the ZIP64 end record and locator.
- Write the end record last, with no comment unless asked for one.
Set version needed to the highest value any feature of the member requires. Write folders as zero-size stored entries whose names end with a slash, and with the folder attribute if you can. A date and time of zero means none, and a time before 1980 cannot be stored. An extended timestamp gives an exact UTC time.
Where readers disagree
A reader that walks the central directory and a reader that walks the file from the start see the same archive only if the writer was honest. They differ like this:
| Situation | Central directory reader | Streaming reader |
|---|---|---|
| A member was deleted by rewriting only the directory | Not listed | Finds it and may extract it |
| The directory lists fewer, more or different members than the file contains | Believes the directory | Believes the local headers |
| Local and central headers differ in name, method or sizes | Uses the central header | Uses the local header |
| A stored member contains bytes that look like a ZIP header | Reads the right data | May stop early, or see an extra member |
| Stored member with bit 3 | Fine | Must search for the end of the data, and can be fooled |
| Bytes before or after the archive, or in a gap (such as the APK Signing Block) | Skips them | May stop at them |
Because of this, a security check run with one kind of reader says little about what another kind of reader will extract. The explorer reads the central directory and reports each place where the local header differs, a gap in the file, or a directory that does not match the record counts.
Zip bombs and overlapping entries
A zip bomb is an archive that expands to a huge size. The classic form nests archives inside archives. A more dangerous form uses overlapping entries: nothing in the format stops several central directory entries from pointing at the same local header or the same region of data, so one compressed block can be counted many times. Declared sizes can also be false.
A defensive reader:
- Sets limits on total output and number of members, and checks them while decompressing, not just from the headers.
- Stops when the output exceeds the member’s declared uncompressed size.
- Tracks each member’s byte range in the file and rejects entries whose ranges overlap. A valid member’s data does not overlap any other’s.
- Does not recursively extract archives inside archives without a limit.
Path traversal
A member named ../../etc/passwd writes outside the target folder if an
extractor just joins it to the destination. This class of bug was named Zip Slip by
Snyk in 2018. The
format forbids it, with no leading slash and no drive letter, but says nothing about
.., and an archive can break any of these rules.
An extractor should reject or clean names containing a .. component, an
absolute path, a drive letter, a backslash, or an empty component. After building the
final path, it should check that the path is inside the target folder. It should also
think about symbolic links: a link member followed by a member written through it can
escape the folder even with clean names.
Duplicate names
Nothing forbids two members having the same name, and archives have been built to exploit that. Different tools then pick different entries, or extract one over the other. Names that differ only in case, or in Unicode normalisation, can also collide on some file systems.
A strict reader should reject duplicate names, or at least make a deliberate choice and state it. The explorer lists every entry and flags problems it finds.
What common tools do
| Tool | Behaviour |
|---|---|
| Python zipfile | Reads the central directory and rejects multi-disk archives. Writing a duplicate name gives a warning; on reading, the later entry wins name lookup, and both stay in the list. On extraction it strips drive letters and roots and drops .. and . components. |
| Go archive/zip | With GODEBUG=zipinsecurepath=0, opening an archive with a non-local name or a backslash returns ErrInsecurePath, and the reader is still usable. The documentation says this may become the default. Duplicate names are appended on write, not replaced. |
| Info-ZIP unzip | Removes ../ components from names by default. The -: option restores the older behaviour. |
| libzip | Writes version needed 20 for LZMA, XZ and Zstandard, with a comment that WinZip refuses other values. |
Java JarOutputStream | Adds the 0xCAFE marker to the first entry it writes. |