Pointing Cargo's .cargo-ok write at a file outside the crate directory
- Identifier
- CVE-2022-36113
- Software
- Cargo
- Affected
- all versions of Cargo (the Rust blog's own phrasing), i.e. up to Rust
- Fixed in
- Rust 1.64, released 22 September 2022
- Reported by
- Ori Hollander, JFrog Security Research
- Disclosed
- 14 September 2022
Go and look in your ~/.cargo/registry/src directory. Every crate Cargo has ever unpacked on that machine is sitting there as an ordinary source tree, and in the root of each one is a file called .cargo-ok. When the bug below was reported, in 2022, that file was two bytes long and contained the word ok. (Since Rust 1.71 it holds a small JSON version marker, {"v":1}, instead.)
It's a receipt. Untarring a few thousand files is not atomic. If the process dies halfway through you're left with a directory that looks like a crate but is missing whatever hadn't been written yet. Cargo would happily compile that, and the failure would be baffling. So Cargo extracts the archive, and then, once every file is on disk, writes ok into .cargo-ok. Next time it wants that crate it checks for the receipt. No receipt, no trust, extract again.
That's a good pattern. Lots of tools do a version of it. The problem is a question nobody asked out loud: what if the archive contains a file called .cargo-ok?
Ori Hollander of the JFrog Security Research Team asked it. Here's the Rust Security Response WG's description, which contains the whole bug:
Cargo allowed packages to contain a
.cargo-oksymbolic link, which Cargo would extract. Then, when Cargo attempted to write "ok" into.cargo-ok, it would actually replace the first two bytes of the file the symlink pointed to withok.
A tar archive can carry a symlink as an entry. The entry has a name and a target, and the target is just a string that the extractor passes to symlink(2). It doesn't have to point inside the archive. It doesn't have to point anywhere that exists yet. ../../../../etc/something is a perfectly legal target, so is an absolute path, and creating the link succeeds whether or not the destination is there.
So the crate ships an entry named .cargo-ok whose target is wherever you like. Cargo extracts it, creating the symlink. Cargo then finishes extracting, opens .cargo-ok for writing, and follows it.
The write primitive#
Worth being exact about the primitive here, because it's an unusually constrained one.
You choose the path. Anything the user running cargo build can write to.
You do not choose the content. It is ok. Always ok.
You do not choose the offset. Opening a file for writing and writing two bytes puts them at offset 0. Not truncating the file, not appending: overwriting the first two bytes and leaving the rest alone.
So it's an arbitrary-path, fixed-content, fixed-offset, two-byte write. That isn't nothing. Two bytes at the head of a file will break a magic number, corrupt the first entry of a config file, or damage a header in a way that's annoying to diagnose. It is also a long way from arbitrary code execution, and Rust rated it low.
If you've only ever used Cargo the normal way, "alternate registry" needs a word of explanation. Cargo can be pointed at package servers other than crates.io, and companies routinely do it. An internal registry carries proprietary crates, or a mirror carries only an approved subset. You configure it in .cargo/config.toml and then depend on foo = { version = "1", registry = "internal" }. The download and extraction path is the same code either way. What differs is who vetted the tarball before it got there.
The severity reasoning as published has two halves. The first is about who can deliver the crate. crates.io "implemented server-side checks to reject these kinds of packages years ago", so packages coming from the default registry can't contain this. The advisory says "Users relying on crates.io are not affected." The risk sits with people using alternate registries. There the recommendation is to "exercise care in which package they download, by only including trusted dependencies in their projects." The second half concerns what a dependency can already do. By design Cargo allows code execution at build time, through build scripts and procedural macros. So the working group treats a symlinked marker file as a more limited way to accomplish what a malicious build script or procedural macro can do anyway.
The zip bomb#
The other issue is the plainer of the two:
Cargo did not limit the amount of data extracted from compressed archives. An attacker could upload to an alternate registry a specially crafted package that extracts way more data than its size (also known as a "zip bomb").
Nothing clever. Cargo streamed the decompressor's output to disk and never asked how much output there had been. JFrog's entry puts it at CVSS 4.8 and calls it low, with the same alternate-registry caveat. crates.io enforces decompression size limits at upload time, and the client didn't enforce anything at download time. That's a recurring split. Registry operators build the check into the front door and the client ends up trusting the door.
Preventing it#
Put the marker somewhere the archive can't reach: a sibling directory, a lockfile store, a database row. Create it before extraction, so the archive's own entry collides with an existing regular file or gets rejected outright. Or open it with O_NOFOLLOW, so that following a symlink is an error rather than a feature.
None of which is a criticism of the receipt. The receipt exists to make extraction safer, and the reasoning behind it holds up. Somebody thought about partial extraction, decided a marker file was the fix, and wrote it correctly. The marker just sat in the one place the archive could reach, two bytes wide, spelling ok.
The symlink write is CVE-2022-36113 and the zip bomb CVE-2022-36114, and both were present in every version of Cargo. Both were fixed in Rust 1.64, released 22 September 2022, eight days after the advisory went out. That build-script argument is also the published reason no point releases were issued. The Rust team put patches against 1.63.0 in the wg-security-response repository for anyone building their own toolchain.
Sources
- 1Security advisories for Cargo (CVE-2022-36113, CVE-2022-36114)blog.rust-lang.org
- 2Rust Cargo zip-bomb DoS (CVE-2022-36114)research.jfrog.com