Yet another npm account has been compromised with malicious code. Sadly, it isn't the first time. So far I've never heard of a similar attack against crates.io . But is that because crates.io is fundamentally more secure, or just luckier? I'd like to believe the former, but I fear the latter. What can we do to prevent attacks like this one? cargo-vet is the best idea I've heard so far, but I think its uptake is low.
The author fell for a 2FA phishing mail targeting his npmjs login.
crates.io uses github as authentication provider. And if you're logged in to github it's just a redirect and back to turn the github login into a crates.io login, which puts a session token in the cookies. A cookie minted today is valid until 8th of december. With that you can mint crate publish tokens.
So an attacker could either obtain someone's github login, a session token or publish tokens from his dev machine.
Crates.io recently gained the trusted publishing feature which reduces the need to have long-lived tokens lying around on your dev machine. But npm has that too.
So I don't think crates.io is inherently much safer against this particular kind of attack.
I think it's a pretty reasonable set of concerns to have, and one that is - or should be - shared by users of any language whose development ecosystem is heavily dependent on public package repositories. Even Perl's venerable CPAN system has had problems with this.
There is very likely an element of degree. Node's development culture strongly encourages publishing small packages and using many packages, from many authors, as a matter of course. Implicitly, by using someone else's code you are trusting them, and the more people you extend that trust to, the easier it is for someone to violate it - inadvertently or deliberately. Rust isn't quite as far along that path as Node is, but by my reckoning it's headed that way, with a rich and diverse ecosystem of packages that do small sets of things well provided by a wide array of authors for a wide range of reasons. That's a good thing on the main, but this risk is one of the tradeoffs involved.
These attacks do not happen in isolation. They happen as a systemic issue, in response to a multitude of factors. That makes headline-level "fixes" tenuous at best, and often worse than nothing in practice.
The major factors as I see them are
- widespread normalization of dependency graphs with exorbitant contributor counts;
- the execution of code from those dependencies during build and test; and
- the existence of high-value targets - in this case, cryptocurrency, but it could be network access, business data, or anything else - that is accessible to those dependencies.
These in turn are all the way they are because developers are just people trying to do people things - jobs, hobbies, and whatever else - and hoping to lean on help from their community in the process. I don't have any suggestions that I'm happy with, nor would I shut down projects like crates.io in response to this, but I think it's a question worth asking regularly.
I would like to suggest using hardware security tokens as an hard-to-compromise second factor (they're domain-bound)... but the way crates-io login and publish-token creation currently works there's no 2FA prompt, it piggybacks purely on an existing github browser session.
Perhaps the "new token" action should optionally support 2FA - that is 2FA on crates.io, not in github - so that creating new tokens can't be done with stolen browser sessions.
I think there is an overlooked angle here in "small" libraries: Consider the kind of library which is
- very small,
- straightforwardly written, and
- not in a problem domain where nearly every bug is potentially security-relevant (like parsers).
In these cases, it is a reasonable option to depend on the library and not routinely accept updates to it until you encounter an actual bug. If you do not accept updates, you are not vulnerable to future versions of that library.
The catch, for Rust, is that Cargo doesn't let you express small-useful-lib = { "=0.2.3" } without also forbidding any other package in the dependency graph from using 0.2.4. There are several ways this could be addressed:
- Allow duplicated dependencies instead of erroring, as long as the deps are private.
- Donât touch the dependency version requirement at all; instead, have a place separate from package dependencies to put rules that constrain what
cargo updatecan do, where âdonât update this unless necessaryâ is an option. Other project-specific rules could include âonly update to versions that an auditing tool has approvedâ (likecargo-vetorcargo-crev).
I hope that someday we can have these options, or others that help work towards software that is managed in ways that encourage it to be secure and boring, instead of vulnerable and exciting. I worry that thereâs a positive feedback loop between:
- âTo fix vulnerabilities, always update everything ASAP.â
- Therefore, because everything is updated constantly, malicious code can take effect quickly and there is less attention available to even consider auditing any individual code update.
- General perception of âif it isnât freshly updated then itâs insecureâ.
Using less third-party code is one way to reduce the volume of code in this loop, but other ways include:
- Distinguishing between dependencies that are large attack surfaces and dependencies that are small to zero attack surface, for your program
- Write code in ways that are less vulnerable to bugs; weâre all already doing that here if your comparison baseline is C++, but there are more aspects than just choice of language.
Inherently, there's nothing that really can stop a bad guy from publishing good useful code until it's popular then adding a backdoor (except all code getting vetted, and that can be gamed by also having another account also getting trust until they approve the backdoor, etc), so all you can really do is make the easy path more work, and the target less valuable until it's not worth it.
At least crates are generally the original source code, which makes obfuscation more obvious than the regularly minified-only npm packages. Otherwise in general they're actually way scarier: npm have been moving steadily away from "automatic" actions that execute on the developers machine only when installing or building code, but this is intrinsic to Rust. Not to mention how easy it is to directly exploit FFI or unsafe to access scary capabilities without obvious external dependencies.
There was some heat a while ago on sandboxing at least proc macros, tied to some controversy around serde, but I didn't really understand what that whole thing was about?
See: Build-time execution sandboxing ¡ Issue #475 ¡ rust-lang/compiler-team ¡ GitHub
This is an accepted Major Change Proposal to âadd support for sandboxed build scripts and proc macrosâ. This states that, at a minimum, the compiler team is interested in seeing this done. Unfortunately, as far as I know, no one is currently doing implementation work for this change.
Interesting, I assume when they mention using Wasm they don't mean shipping a build.wasm but an on the fly compile to wasm. I'd expect it would be somewhat reasonable to use MIRI as a sandbox instead, given we're in Rust and know we don't have the typical interpreter risks of the target escaping via memory safety issues, but perhaps it's simply too slow?
Miri is sufficiently slow that you may have to think carefully about which tests are small enough to practically run under it â so you definitely wouldnât want to run all proc-macros and build scripts with Miri. Iâm not sure how much faster it would be without the UB checks[1] but there are definitely cases where you want full speed computation at build time.
Miri is essentially an unrestricted version of the
constinterpreter, and in fact thereâs an unstable option which turns off all âyou canât do this in const eval yetâ restrictions thatâs called-Zunleash-the-miri-inside-of-you. âŠď¸
Yeah I was definitely thinking of "MIRI the interpreter library" not cargo MIRI as it currently exists, but I wouldn't be too surprised if it had very limited performance even without the checks (which may not be ok to turn off in any case if we're taking about sandboxing! maybe if you figure out how to ban unsafe)
Unfortunately, most of the things that you can do to make interpreters faster also make them much less secure...
In the end, it's a matter of trust. We not only trust that developers of our dependencies will not intentionally introduce malicious code, but also that they will properly protect their publishing credentials. (There are also infrastructure risks, but it's a separate discussion.)
Helping with protection of credentials can significantly reduce risks (promoting hardware tokens, supporting threshold signatures, even simply using password-based encryption of local Cargo token for God's sake!). We also could do a better job with isolating developer environment from built code. But the only fundamental solution is to reduce number of people/groups whom we trust.
Reducing number of dependencies is an obvious solution, but obviously not practical or economically viable for most developers. But there is another approach which could be viable for the Rust ecosystem: shared review databases. Imagine Rust/Cargo/Clippy/etc. teams and Mozilla/Google/Microsoft/Amazon/etc. publishing registry of crate versions which they use and which were reviewed by them.
Now instead of trusting random developers you could trust a smaller number of big players to (hopefully) vet a big chunk of your dependency tree, while you could manually review the remaining crates as part of your private database. If a malicious actor releases a new patch version of one of your dependencies it will not affect you, since your dependency tree contains only vetted dependencies.
cargo-crev is a very important step in this direction (though I am not sure that its proof distribution model is the most optimal solution), but it's adoption unfortunately is somewhat disappointing...
Issue open for 11 years now:
WASM seems like an ideal environment for isolating the execution of build.rs scripts and procedural macros during compilation.
Yeah I meant from the way it's phrased in that document it's pretty ambiguous in what way they were thinking it would work. Shipping already built .wasm has been raised and tried before (the serde controversy I mentioned earlier)
I mentioned MIRI mostly because it's already there and would maybe make it a bit easier to point to where the issue happened, maybe even to allow unsafe without allowing really scary obfuscation.
Not familiar with the WASM attempt. Sounds interesting. What is this serde controversy; is there a link?
Searching for something like âserde precompiled binariesâ should find you a lot of information about it, including articles written about it at the time.
Someone must have read this thread and thought âchallenge acceptedâ? ![]()
I do not know, I simply do not use Cargo considering - it's unsafe. I plan also to switch to GCC Rust compiler when it reaches 1.0 milestone.
A good news, hackers do not target Rust, so you shouldn't worry.
Um, don't use NPM/npm? Read the source code on GitHub/GitLab, fetch from GitHub/GitLab directly, bundle with bun build or deno bundle, once, done.
I have not used npm in around 2 years, unless I compile npm to a single executable for single use then delete the executable because an NPM/npm contributor said this Can this be used to create/download npm as a standalone executable? ¡ Issue #3237 ¡ nvm-sh/nvm ¡ GitHub
npm canât be eliminated from the equation when youâre dealing with JavaScript ÂŻ\_(ă)_/ÂŻ
In general, once you have a working version of your code you can bundle to a single script, then make changes to that script as necessary, without ever using NPM/npm to fetch the source code again.
It's kinda wild that folks blindly fetch and re-fetch the same packages over and over again from repositories for no reason. N downloads, and all of that. For what? Bundle working code once, and be done with the fetching external code thing.