Solta
Blog

Opening an archive somebody else made

By

We added folder upload this week. The unzipping took an afternoon. Deciding what to refuse took the rest of it.

A ZIP file is not data. It is a set of instructions from a stranger about where to write files and how much memory to use, and the default behaviour of every unzip library is to follow them.

Names that climb out

An entry can be called ../../etc/passwd. If anything joins that to a directory, it lands somewhere nobody intended. This is old — it has a name, zip slip — and the usual advice is to sanitise the path.

We refuse it instead. These names become URLs on the site's own origin, and a name we would have to clean up is a name we did not understand. Sanitising turns a hostile input into a plausible one; refusing turns it into a message that says which file was the problem.

Small files that are not small

Forty kilobytes can decompress to four gigabytes. The obvious check — unzip it, then look at the size — happens after the memory is already gone.

So every size decision is made against the sizes declared in the archive's own headers, before a byte is expanded: the total, each file, the file count, and the ratio between compressed and uncompressed. A four-megabyte file of zeroes compresses to under sixty-four kilobytes, and is refused on that ratio alone.

The declared sizes are also a stranger's claim, of course. They are a claim we can check cheaply and one that only gets us into trouble if it understates — which the streaming reader catches on the way through.

An allow-list, not a deny-list

The extensions we serve are listed. Everything else is refused by name, with the extension in the message so the person knows which file to remove.

A deny-list would be a promise to have thought of everything. We have not, and neither has anyone. A static host that serves .php is not a static host, and the way to be sure is to name what goes in rather than what stays out.

The part that was not about safety

People zip a folder, so everything arrives under one directory nobody meant to be part of the URL. One shared root is stripped. Two or more means the archive really is laid out that way, and it stays.

And a folder with no index.html used to be an error. It is not one: a folder of images, or a report and its spreadsheet, is a perfectly reasonable thing to want a link to. We write the page it opens on — a list of what is inside, with every name escaped, because those names came from a stranger and it is served from the site's own origin.

The tests read like a list of things a stranger's archive is not allowed to say. That is the most useful shape we found for them.

Solta publishes what your AI builds

React included, with a link that does not move and the people you send it to need no account.