Library / SDK
thejoshwolfe/yauzl avatar
thejoshwolfe/yauzl

yauzl: a zip reader for node that follows the specification

yet another unzip library for node

831 stars84 forksJavaScriptMIT

At a glance

What is it?
yauzl reads zip archives in node with five design principles stated up front, among them never block the JavaScript thread, keep memory use under control, never crash on a malformed file, and catch unsafe file names. It offers four ways in, from a path to a random access reader you implement yourself, and defaults to reading the central directory rather than scanning local file headers.
Who is it for?
Adopt yauzl if you need to read zip archives in node and care about the malformed input cases, because the library refuses to crash on a bad file, gives you the error instead, and ships a name validator for the classic path traversal problem. Do not adopt it if you need to write archives, because this is the reading half and the writing half is a separate project, yazl.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 141 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Five principles, stated before the API

yauzl is yet another unzip library for node, and the for zipping you want a different library, yazl, which is a separate project by the same author.

The README opens with a design principles list, and it is more useful than the API documentation that follows, because it tells you what the library will refuse to do and why.

The first principle is to follow the spec. That means not scanning for local file headers and instead reading the central directory for file metadata, and the README links a section titled No Streaming Unzip API, which is the reason: the format is defined around a central directory at the end of the file, so a reader that wants file names has to seek to the end first. A library that scanned the file sequentially could stream, at the cost of a format that is not the format.

The second is not to block the JavaScript thread, with async APIs provided and used. The third is to keep memory usage under control, which the README states as not attempting to buffer entire files in RAM at once.

The fourth is never crash, if used properly, with the explanation that malformed zip files should not bring down client applications that are trying to catch errors. And the fifth is to catch unsafe file names, pointing at the validateFileName() function.

That last one is the security principle, and it is the one that most affects a user, because a zip entry called ../../etc/passwd is a standard way to turn an unzip into a file overwrite.

Four ways in, and why one of them exists

There are four ways to get a ZipFile, and each corresponds to a different place the bytes come from.

open(path, options, callback) calls fs.open with the path in read mode and reads the file descriptor effectively the same as fromFd would. fromFd(fd, options, callback) reads from a descriptor that is presumed to be an open zip file, with an important constraint noted in the README: random access is required by the zip file specification, so the descriptor cannot be an open socket or any other descriptor that does not support random access. That single sentence rules out the obvious streaming use case, and it is a property of the format rather than a limitation of the library.

fromBuffer(buffer, options, callback) is like fromFd but reads from a RAM buffer. It has two documented differences: a ZipFile acquired this way never emits the close event, and calling close() is not necessary. The autoClose option is ignored for this method.

The fourth is the interesting one. fromRandomAccessReader(reader, totalSize, options, callback) allows clients to implement their own back-end file system, and the example given is a client that translates read calls into network requests. The reader must be a subclass of RandomAccessReader that implements the required methods, and the totalSize is a Number giving the total file size. So the library is not tied to a local file: you can point it at an object store over HTTP and it will do the seeking.

Each of the four has a Promise variant, with the names openPromise, fromFdPromise, fromBufferPromise and fromRandomAccessReaderPromise, so the promise and callback styles are parallel rather than one being a wrapper over the other.

The options that decide how much memory you use

The options are the interesting part of the API, and every one of them trades something for something.

The defaults for open are autoClose true, lazyEntries false, decodeStrings true, validateEntrySizes true and strictFileNames false, and the same set appears for fromRandomAccessReader. The defaults for fromFd differ in one place, with autoClose false, and fromBuffer ignores autoClose entirely and defaults the rest the same way. The reason for the difference is obvious once you understand what autoClose does, and the README shows it in the equivalent form of closing the file on the end event.

lazyEntries is the option that matters most. When it is false, entry events will be emitted as fast as possible to allow piping file data from all entries in parallel. The README is direct that this is not recommended, because it can lead to out of control memory usage for zip files with many entries, and it links issue 22 as the discussion. When it is true, an entry or end event is emitted in response to each call to readEntry(), which allows processing of one entry at a time and keeps memory usage under control for archives with many entries.

So the default is the fast one, and it is the one that can blow up memory. For a service that opens archives supplied by users, lazyEntries true is the setting you want, and it is the reason the callback example in the README passes {lazyEntries: true}.

decodeStrings is the default and causes yauzl to decode strings with CP437 or UTF-8 as required by the spec. Turning it off has three consequences listed in the README: the comment, the file name and the file comment become Buffer objects instead of Strings, any Info-ZIP Unicode Path Extra Field is ignored, and automatic file name validation is not performed.

The backslash bug, and why the default is lenient

strictFileNames is the option that encodes a specific piece of real-world history, and it is a good example of a library making a judgement call and documenting it.

The spec forbids file names with backslashes, but Microsoft's System.IO.Compression.ZipFile class in .NET versions 4.5.0 until 4.6.1 creates non-conformant zip files with backslashes in file names. When strictFileNames is false, which is the default, and decodeStrings is true, all backslash characters in each file name are replaced with forward slashes. When strictFileNames is true and decodeStrings is true, entries with backslashes result in an error. And when decodeStrings is false, strictFileNames has no effect.

The default is lenient on purpose, so that clients can read those non-conformant files without knowing about the Microsoft-specific bug. The cost of the default is that a file name which genuinely contains a backslash is silently rewritten, which matters if you are extracting to a filesystem where a backslash is a legal character.

The other two string-related options are the Info-ZIP Unicode Path Extra Field, which is ignored when decodeStrings is off, and the file name validation, which is skipped in the same case. So the three behaviours are switched together, and the README is explicit that turning off string decoding also turns off the safety check on names, which is a trade a security-conscious user needs to know about before making it.

Zip bombs: the size check is not optional

validateEntrySizes is on by default, and it is the entry that makes a library like this safe to point at untrusted archives.

The check ensures that an entry's reported uncompressed size matches its actual uncompressed size. The README is careful about when it happens, because the timing is the point: the check happens as early as possible, which is either before emitting each entry event, for entries with no compression, or during the readStream piping after calling openReadStream(). The README points at the openReadStream() documentation for more information on defending against zip bomb attacks.

The reason for the two timings is that the uncompressed size is only known for real once the data has been inflated, and for a stored entry it is known from the metadata. A library that checked the size only at the end of the stream would have already written the bomb's output to the destination by then. Checking before the event for stored entries, and during the pipe for compressed ones, means the caller can stop the stream before the damage is done.

This is the one option in the list that has a security purpose rather than a convenience purpose, and the README does not offer a way to turn it off without saying so explicitly, which is a sign that the default is intended to stay.

The error contract, and the package that ships three files

The error handling is the last of the principles, and the way it is expressed is instructive.

Every optional callback parameter has a default, and the default is to throw:

js
function defaultCallback(err) {
  if (err) throw err;
}

So a caller who passes no callback gets the throw, and a caller who passes a callback decides what to do. The promise API throws inside the await, which is why the README's example wraps the whole loop in a try block and rethrows, with the comment that it indicates a malformed zip file or an I/O error.

The specific error for a file that is not a zip is documented on open. An error is provided if the End of Central Directory Record cannot be found, or if its metadata appears malformed, and the README notes that this kind of error usually indicates that this is not a zip file. Otherwise the second argument is an instance of ZipFile.

The package itself is tiny, and the manifest explains why. The name is yauzl at version 3.4.0, the main entry is index.js, and the published files are crc32.js, fd-slicer.js and index.js, three files. The runtime dependency list has one entry, pend, a tiny promise helper, and the development dependency is bl, a buffer implementation, which is what the test suite uses to compare against a reference implementation. The engines field asks for node 12 or later, and the test script is a single command, node test/test.js, with no test framework.

The examples directory has four scripts, including compareCentralAndLocalHeaders.js, which is a nice piece of self-documentation for the first design principle, plus dump.js, promises.js and unzip.js. The licence is MIT, the default branch is master, the last push was on 2026-06-07, and the project publishes no GitHub releases, so the version in package.json is the thing to track.

Editorial conclusion

Adopt yauzl if you need to read zip archives in node and care about the malformed input cases, because the library refuses to crash on a bad file, gives you the error instead, and ships a name validator for the classic path traversal problem. Do not adopt it if you need to write archives, because this is the reading half and the writing half is a separate project, yazl. Before you use it, decide whether you want the lazy or the eager entry mode, because the default emits entries as fast as possible and the README warns that this can lead to out of control memory usage on archives with many entries, and read the strictFileNames note before you assume a backslash in an entry name is a path separator on every platform.

Frequently asked questions

What is yauzl and what does it do?

It is a zip reading library for node, described as yet another unzip library for node, with a promise API and a callback API. It follows the zip specification by reading the central directory rather than scanning local file headers, streams entries rather than buffering whole files, and refuses to crash on malformed archives.

How do I read a zip file in yauzl without running out of memory?

Pass the lazyEntries option, which makes the library emit one entry or end event per call to readEntry() so you process one entry at a time. The default emits entries as fast as possible to allow piping in parallel, and the README warns that this is not recommended because it can lead to out of control memory usage for archives with many entries.

How does yauzl protect against zip bombs?

The validateEntrySizes option is on by default and checks that an entry's reported uncompressed size matches its actual size, as early as possible: before the entry event for stored entries, or during the read stream piping for compressed ones. The README points at the openReadStream documentation for more on defending against zip bomb attacks.

Can yauzl read a zip file from somewhere other than a local file?

Yes. Besides open for a path, fromFd for a descriptor, and fromBuffer for a RAM buffer, there is fromRandomAccessReader, which lets a client implement its own back-end file system, for example translating read calls into network requests. The descriptor variant requires random access, so it cannot be a socket.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. thejoshwolfe/yauzl on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/thejoshwolfe-yauzl.svg)](https://hysenlabs.com/projects/thejoshwolfe-yauzl)

Was this useful?