Do not run any Cargo commands on untrusted projects

Sergey "Shnatsel" Davidoff
5 min read6 hours ago

--

TL;DR: Treat anything starting with cargo as if it is cargo run.

Background

Cargo is the build system for the Rust programming language.

It is common knowledge that you should not invoke cargo run on random code from the internet, because that will run the code.

It is reasonably common knowledge that cargo build will also run arbitrary code. It is a build system, after all! They run code to build code.

I’m here to tell you that any command starting with cargo can run arbitrary code when operating on an untrusted repository, and should be treated the same as cargo run.

This shouldn’t be surprising to people coming from other languages. For example, anything starting with make also runs arbitrary code. Cargo just takes care of things by default so you hardly ever have to inject your own code - but the fact that you don’t have to doesn’t mean you can’t.

Attack vector #1

Cargo reads its configuration from a number of files, some of which can be defined right in the repository, e.g. in .cargo/config.toml.

Among other things, you can redefine the path to the Rust compiler:

[build]
rustc = "/usr/bin/rustc" # path to the rust compiler

So you can also put a malicious script in the repository and do this:

[build]
rustc = ".cargo/fake_malicious_rustc"

which will be called in more situations than you might think: not only on cargo build, but also cargo clean, cargo update, cargo metadata. In fact, I’m not aware of a single Cargo subcommand that doesn’t call rustc.

And rustc is not the only problematic field. There’s also rustc-wrapper, rustc-workspace-wrapper, the linker configuration, and the entire [env] section that can be used to override environment variables for processes that Cargo launches, so you could override $PATH and get arbitrary code execution that way.

“But wait,” you might think, “I’m using this cool third-party plugin for Cargo, and it’s not reading the Cargo configuration. Surely running that is fine?”

Attack vector #2

There are many Cargo plugins meant to help with supply chain security. There’s cargo audit, cargo cyclonedx, cargo vet, cargo crev, and so on. They aren’t even part of Cargo, and they shouldn’t read the config files.

This doesn’t mean that they are safe to run. When you run cargo audit, what gets called is Cargo which then invokes the actual cargo-audit.

That is, unless when you put this in .cargo/config.toml in the repository:

[alias]
audit = "run"

which makes it so that Cargo turns the cargo audit you typed into a cargo run behind the scenes and it’s arbitrary code execution all over again.

Disclosure

I reached out to the Rust Security Response WG and a member of the Cargo team. The consensus was that this behavior is not an entirely new discovery and this behavior is not going to change in the near future, so it should just be better documented.

There is also a desire to eventually remove attack vector #2, but it will require a deprecation period since it’s a breaking change for people who override Cargo plugins for a legitimate reason, and there is no clear timeline for it as yet.

I also looked up what Cargo plugins had bug bounty programs. I reported attack vector #1 to Mozilla who maintain cargo vet, but they concluded it’s not really a problem for their intended use cases.

There’s also a bug bounty for cargo cyclonedx, but for obvious reasons it excludes maintainers, and I am a maintainer of cargo cyclonedx (along with cargo audit, which is why I’ve been using it as an example above. I maintain way too many Cargo plugins). So no bug bounty money for me.

So is this a problem?

This behavior is kind of by design, and isn’t really different from how build systems work in other programming languages.

It’s the existence of Cargo plugins that makes the situation murky. And whether it’s a problem or not should be decided on a case-by-case basis.

It is definitely a problem for cargo crev, which explicitly treats the code it’s running on as untrusted and lets the user review it before running. The code under review being able to misrepresent itself is a problem. They will have to become simply crev or rust-crev or something else that doesn’t involve getting called through cargo.

It’s not really a problem for cargo cyclonedx because in the long term it should become a wrapper for cargo build anyway. Although we’ll probably have to shutter the bug bounty program: if we assume the code you’re running it on is trusted, then there aren’t any attack vectors at all.

And this puts cargo audit in a really awkward spot. Most people just run it on their own projects, which is entirely safe. On the other hand, similar tools like osv-scanner or trivy do not execute arbitrary code from the repository, so why should cargo audit?

I even wrote a custom binary format parser to make its binary scanning 100% memory safe and robust, so it feels weird to get an arbitrary code execution vulnerability from a far simpler source.

I don’t really know what the right answer is for cargo audit.

Workarounds

The good news is that attack vector #2 can be avoided right now by calling a Cargo subcommand directly. For example, instead of cargo crev you would type cargo-crev crev (yes, ‘crev’ appears twice) and that fixes it.

It’s still a terrible default and completely untenable except as a quick and dirty patch, but it’s something.

The bad news is that there is no robust workaround for attack vector #1, and it affects most third-party subcommands too because they still call Cargo behind the scenes.

You can override individual configuration keys in a way that takes precedence over config.toml files, but this will never be airtight. There are too many individual configuration keys that could cause problems (looking at you, [env] section), and more of them can be added over time.

You could try deleting all the configuration files in a given project. But it’s not safe to put them back in once you’re done, otherwise two instances of a tool that does that running in parallel would still be vulnerable: one instance can delete the files and then put them back in before the other instance is finished running, leading to arbitrary code execution. And you also would need to ensure only you have permissions to write to these files, otherwise it’s a local privilege escalation vulnerability. This approach is too brittle to be practical.

So the best advice I can give is in the title of the article: Do not run any Cargo commands on untrusted projects.

--

--

No responses yet