After mastering Rust in smaller and bigger hobby projects, I wanted to use Rust in production. But my boss asked me about the danger of "supply chain attacks" in Rust. Sadly, the Rust crate ecosystem is "potentially very dangerous". I didn't have any bad experience till now, but the doors are widely open for malevolent actors! It is enough to get a ransomware attack only once to make life miserable and to destroy the reputation of the Rust crate ecosystem. This bad reputation will then stick with Rust for eternity.
My goal here is to make it easier to review the "potentially harmful" code of all the crates in the dependency tree of my Rust project.
There can be hundreds of dependency crates. I believe it is my responsibility to manually review every single one of them exactly the version that is resolved by cargo and used for compilation. Then with the tool "cargo-crev" I can write a subjective opinion why I think that a crate version is safe to use in our project. And finally show this list of reviews to my boss for approval.
To make this feasible, we must reduce the code to review to a minimum - just the "potentially harmful" code. Maybe this is not perfect, but it is much better than what we have today.
The standard library is one "monolithic crate". It is used by default. No need to write anything in the Cargo.toml. It has functions that can read/write files, send data to the network, run commands, ... All very "potentially harmful" operations, ideal for a ransomware attack.
We can only choose to disable the dependency on it with
#[no_std]. That means that we then use a minimal subset of "std" called "core". We loose most of the functionality of the "std" library.
We can than add some functionality back with crates like "alloc" or "heapless" and crates for I/O operations and other.
I would like to propose the idea to add "features" to the standard library. Features are used widely in crates to enable or disable special functionality and are "part of the Rust language". These features could enable or disable some "std" functions that are potentially harmful and can lead to "supply chain attacks". This would make it easier to review just the code that is "potentially harmful".
Still, we need to review every dependency crate in the dependency tree, but the amount of critical code becomes much smaller and more manageable. Tools like "cargo-crev" should then help to make these reviews available to other developers and our bosses.
I don't want to make here a thorough analysis of all the use cases. Just a simple one that is representative of the idea. I expect that Rust developers will have a lot of good ideas around this concept.
First let's solve the backward compatibility issue:
the "default" feature would have all functionality enabled, just like the "std" library does today.
The first feature is "core". Used alone is the same as today
[std] version = "x.y.z" default_features = false features = ["core"]
Probably the first "potentially harmful" operations to isolate from the "std" library is "fs_write". Without the feature "fs_write" this functions cannot be used:
std::fs::create_dir and a lot of other similar functions.
Similarly the feature "fs_read" isolates the functions
std::fs::read_dir and a other similar functions.
With the feature "fs", the crate can still manipulate file objects. Just the critical functions like creating, opening, reading, and writing files are not included in "fs".
We write a Rust project and add a "simple third-party dependency crate" that cannot read/write files. Our code has the responsibility to create and pass a File object to the "third party dependency crate" for manipulations. After that, it returns the File object to our code, where we can inspect and error handle and eventually write the file. So we have full control of what files and directories are manipulated.
The dependency trees in real-life projects are much more complex than this. We cannot review the code of only the dependency that we add to our project. We must inspect all the dependencies that come with it until we come down to the "std library". All the dependencies down the tree are "potentially harmful".
In Cargo.toml of the simple library crate we can enable these features when it is appropriate:
[std] version = "x.y.z" default_features = false features = ["core", "fs", "fs_read", "fs_write"]
All the operations in "std" that are "potentially harmful" should be isolated behind a feature: fs read/write, network IO, environment access, std::process:Command, ...
Further, also things like FFI to external C code, asm code, and even the use of "unsafe" should be enabled with some kind of "feature".
These features are meant only for the direct dependency on the "std library" for every crate separately. There is no need to complicate around how to make these features transitive through the dependency tree.
The main goal of these features is to simplify the manual review of every dependency crate we use. It is a colossal task, but could be manageable with the help of tools like "cargo-crev".
We can use
cargo tree to find exactly the versions of all the crates we depend on.
First we review the "leafs of the tree" crates that have only the "std" dependency. We just look at the enabled features in Cargo.toml. If no "potentially harmful" feature is enabled on the "std" dependency we can be confident, that the crate cannot do any harm.
When a "potentially harmful" feature is enabled, we can inspect thoroughly only the code that uses that feature. This is easy to find. If we disable the feature in Cargo.toml, the compiler will show errors where the feature is used.
Secondly, we go to the "caller crates" and inspect how they use the "std" library. Then we inspect the calls to the dependency "potentially harmful" functions, that we already know. And so on. Not easy, but possible.
I am sure that with time, good crate design will have the "potentially harmful" code specially isolated in modules and thoroughly commented for the review to be easier. It is in the interest of the crate author to make the review easier to boost confidence in the crate security.
I think there is no alternative to manually reviewing third-party code in the open-source community. It must be done for every crate version we use. Even a simple small "security update" of a mini crate can be a surprise "malicious" version. Who knows?
When any dependency for any reason needs to increment the version it must be first manually reviewed. Automated updates of dependencies by CI systems are super dangerous and should be avoided. Instead, we need a security warning from tools like RustSec! Then we review the code of the new version and finally change the dependency manually in our code.
To be productive, we need to use third party crates in Rust. It is not realistic to write everything in house. The standard library is really small and this is on purpose. But it is impossible to review all the code of all the crates in search for a few lines of "potentially harmful" code.
There must be a better way to reduce the amount of code that needs to be inspected. One step that Rust already made in this direction is to introduce "unsafe" blocks. They are really easy to find and inspect for soundness. This is great for memory corruption! But what is more dangerous for "supply chain attacks" : a memory corruption in "unsafe" or a simple file-write to encrypt all of your documents and ask for a ransom?
The latter can be difficult to find, isolate and inspect. If only this "potentially harmful" operations could be enabled/disabled with a "feature" in the standard library.
We are also worried about other "potentially dangerous" aspects of Rust like the fact that build.rs and procedural macros can run any code on my development system in build time, even in edit time with the use of "rust analyzer" or similar tools. We really don't feel safe about the third party crate ecosystem of Rust and that must be addressed.
In the past years I have read a lot of discussions around this topic in Rust.
Sadly I didn't find anything is moving in the right direction. If I am wrong, please inform me.
I found a lot of skepticism and negativism with little constructive efforts or signs of approval:
"This is not a perfect solution, than it makes no sense to do anything.",
"Nobody had this problem till now, don't panic around nothing",
"This is bad and will just make a false sense of security",
"Don't demonize the unsafe, some unsafe is sound",
"Other languages had tried and failed",
"Just use a sandbox like WebAssembly engine or Docker",
"If I add a dependency to my code, I don't want to check all transitive dependencies",
"The responsibility to review the dependency code is on the author of the library, not me",
"Transitive dependencies need a super complicated effects system or Capability-based security",
"The burden on the author of the library crate is too big",
"The quantity of strangeness in Rust is high, this will add strangeness and break the adoption for new developers",
"There are much more urgent and important things to be done for Rust",
"It is impossible, just stop thinking about it", ...
Here is a good discussion with a lot of links to other discussions on this topic: