Managing Rust Dependencies for Supply Chain Security
Table of contents
As this post got a bit dense, these are my main takeaways for reducing supply chain risks:
- Reduce dependencies by adding only essential features and exploring lighter alternatives.
- Use lib.rs including
cargo audit,cargo crevandcargo vetresults to vet trustworthiness. - Implement CI checks using
cargo denyto automate policy enforcement. - For high-risk projects, consider vendoring dependencies with
cargo vendor.
Managing dependencies is hard in any software project: Which dependencies should you choose, and when is it worth pulling in an external crate? How to keep track of updates, vulnerabilities, or a dependency ending up unmaintained? When building a product, this is not only a quality issue but with EU regulation like NIS 2 or CRA this even becomes a liability issue. This is not only about vulnerabilities that need to be fixed, but dependencies have become a direct cybersecurity target where malicious code is somehow injected into the software “supply chain”. This is a known problem, especially for languages with thriving package ecosystems.
So how is the situation in Rust?
A lot has been said about the amount of dependencies that quickly get drawn even into smaller-sized Rust projects.
My educational project simple-ssg-rs has barely any functionality but had 35 dependencies (plus 7 dev dependencies for testing) before having had a closer look at this topic.
Let’s have a look at how to make an informed choice about which Rust crate to depend on, what the actual risks are with dependencies, and what we can do about it.
Getting Rust Crates
crates.io is the official crate directory supported by the Rust Foundation. I struggle a bit with the search function, though, and feel crates.io does not provide much help in vetting crates. Currently, security features seem somewhat limited in general but this is being worked on. Just recently, crates.io received Trusted Publishing which reduces the threat of API token theft for crate publishers. This is a great feature, even if I fear the implementation might tie crates.io even tighter to GitHub1.
Speaking of GitHub, you can find the hand-curated lists blessed.rs and awesome-rust there. Both have an open process of updates via issues / pull requests on GitHub but focus on crates being popular2 3, and personally I think they provide helpful pointers but not the full answer.
An alternative catalog to crates.io is lib.rs. It catalogs “almost all” crates from crates.io and a few additional ones. How it operates is well described and it provides some interesting stats. More importantly, I find lib.rs provides considerably more information that is helpful for evaluating a crate. I will get back to lib.rs further below.
How to Evaluate a Crate?
The most comprehensive guideline I found is Embark’s approach, and they even provide the open-source cargo plugin cargo deny to automate checking for several of their criteria. The criteria are thorough, well-motivated and grouped into Licensing, Maintainers, Unsafe, Auditing, Testing & CI, Clippy, Rustfmt, Dependencies, Change management, Sponsorship & support. Instead of reproducing them here, I recommend to have a look and will focus on the supply chain security side of things in the following.
Rust Supply Chain Threats
As mentioned in the beginning, not everyone is happy about the amount of dependencies drawn into average Rust projects. One of the main issues is supply chain security: many dependencies mean many potential security threats. The Rust Foundation Security Initiative had a look into what these threats actually are, the threat model of the crate ecosystem is openly available and worth a read. The highest-priority threat identified is a malicious crate being served by crates.io (Severity: High, Likelihood: High, Complexity: Low), and it is a big one as stated in the threat model (excerpt, emphasis mine):
As the ecosystem grows, the likelihood of such attacks will exponentially increase.
In the event a Rust user or developer utilizes a malicious crate, an attacker is currently able to compromise any machine which parses, builds or executes a project which includes that crate as a dependency. All current cases of attacker will lead to simple and direct code execution on the affected system.
Attack Paths for Malicious Crates
The threat of a malicious crate being served by crates.io provides several attack paths which are explained in the threat model, but again I want to focus on the highest-priority one: build.rs code execution.
build.rs files are build scripts written in Rust and commonly used, for example to compile non-Rust code or link to C libraries.
This attack is an example that does not require the project with the malicious crate to be executed, but building it is sufficient for a successful attack.
Quoting the threat model:
Any given Rust project folder has the ability to execute arbitrary code on a compiling system via the
build.rsfacility - which occurs prior to compilation of the project and is executed locally on the system.build.rsprovides complete access to the local system under the executing user as well as complete access to the Rust standard library by default as well as anybuild-dependenciesdefined by the user.Although commonly utilized for customized build processes, this provides the highest probability and path of least resistance for an attacker.
That does sound pretty bad, doesn’t it?
A mitigation in the form of “ecosystem scanning” is being worked on, but given the risk I think it is a good idea to have a look at your dependencies’ build.rs files.
For example when adding a new dependency, or when the dependency is updated and the build.rs changes.
Procedural Macros
An equally high-risk attack path is “Malicious procedural macro”.
Procedural macros can provide a lot of convenience to the user of a library.
For example, I found clap’s derive macros great: you simply annotate the structs and enums holding your command line parameters and clap will generate the command line interface for you.
The problem is, that this again allows arbitrary code execution at build time.
This attack path is similar to build.rs code execution above but malicious code potentially even harder to detect as it might be more obfuscated and distributed over several files.
To this attack path, there is no additional mitigation other than “ecosystem scanning” mentioned above.
That a crate uses procedural macros is quickly detectable by looking at the crate’s manifest, which would contain lines like this:
[lib]
proc-macro = true
Having a look at procedural macros of a dependency is an equally good idea as mentioned for build.rs, even if admittedly not always easy to do.
If you want to have more practical examples on how malicious crates might look like, I can recommend Sylvain Kerkour’s blog post “Backdooring Rust crates for fun and profit”. Even having looked at just one threat and two attack paths, I feel quite the urgency to double check my dependencies. Shall I really audit everything myself? Fortunately, there are things we can do to reduce the risks of ending up with malicious dependencies.
Practical Tips for Reducing Risks of Malicious Dependencies
Let’s try to gather practical tips for reducing the risks discussed above. In the following, we will look into reducing the number of dependencies, increasing the trust in those that remain, and enforcing a policy using automation.
Fewer Dependencies
Each dependency adds a certain risk of ending up with unmaintained or in the worst case malicious code. So naturally, you should only add the dependencies that you really need. Armin Ronacher even argues that we should build more ourselves instead of an over-reliance on external dependencies. This is definitely worth considering but chances are that you do not have the resources to build everything that you need in-house, and you will still end up with a number of dependencies.
cargo add X --no-default-features
I mentioned earlier that simple-ssg-rs has barely any functionality but 35 dependencies (plus 7 dev dependencies for testing).
It turns out that several of them are not even needed.
Following the tip given in John Nunley’s blog post “How to deal with Rust dependencies”, I removed all my dependencies and re-added them using cargo add X --no-default-features (for each dependency X).
Cargo features are optional dependencies of a crate and --no-default-features makes sure that none are activated by default.
As I was already using features of several crates, I had to re-add features using cargo’s -F flag, until my project compiled again.
The build output was quite helpful for this.
Eventually, this brought me down 9 dependencies to 26 total (plus 5 dev), where clap_derive accounts for 6 alone.
Given the discussion of procedural macros above, one may wonder whether the increased convenience over using clap’s builder API is worth it.
John further suggests that “For many dependency-heavy crates, there’s usually an alternative crate with much fewer dependencies, often while retaining the core functionality you depend on.”, which I completely agree with.
Increased Trust
Chances are that after reducing the number of dependencies, we still have a number of them.
For these, we will have to do a background check.
A good starting point is the trifecta of cargo audit, cargo crev and cargo vet.
cargo auditwill check your dependencies against the RustSec advisory database and will warn you about known vulnerabilities or yanked versions.cargo crevis a “distributed code review system” where anyone can provide cryptographically-signed audits in order to build a web of trust.cargo vetis also about manual audits but focuses on enterprise environments where you trust specific organizations / sources of audits, say Mozilla or Google.
Instead of running the three tools individually, lib.rs provides an audit page for each crate which aggregates the information coming from cargo audit, cargo crev and cargo vet.
For example, this is the page for the serde crate: https://lib.rs/crates/serde/audit.
Digging Deeper
If you want to dig deeper, cargo supply-chain will provide information on which people or teams can publish updates to the crates in your dependency graph to crates.io.
Whenever there is an update, you might not want to audit the whole crate again, instead, diff.rs provides a convenient interface to compare the changes between two different versions of a crate.
For example, to quickly check whether something changed in build.rs or procedural macros for a crate where you audited a previous version.
Crate Provenance
Further, there are no guarantees that the code published in a crate matches the crate’s repository. Adam Harvey provides a website to conveniently check that this is the case: https://lawngnome.github.io/divine-provenance/. The methodology and results are further discussed in a blog post.
Scorecards
A notable mention is also deps.dev by Google, which not only works for Rust but also Python, Go, Ruby, etc. ecosystems. For a given package, it provides a score that is calculated from several sub-scores like “Maintained” or “Dangerous-Workflow”. I did not read up on the details of how the sub-scores are calculated but, each provides a reasoning and deps.dev overall follows the OpenSSF Scorecard. I found the overall score quite low for several highly-respected crates but looking at the sub-scores can definitely give some interesting insights.
What About Unsafe?
You may wonder, and it seems unsafe code has a bit of a stigma in Rust.
Of course, this is where potential memory safety issues lie and you want to be conscious about its use but there are perfectly good reasons to use unsafe.
This is especially the case in systems and embedded programming.
In any case, cargo geiger is a tool which tells you which of your dependencies uses unsafe code plus some statistics.
It will also tell you whether #![forbid(unsafe_code)] was defined for the crates that do not use any unsafe code, which is a compiler directive that completely prohibits the use of unsafe Rust code for the annotated scope.
The crates that do include unsafe code might have been audited within the Rust Safety Dance repository on GitHub.
In this case, the discussions under the respective issues will provide more information.
Automation
So far, we looked at what we can do individually to increase our trust in a certain crate, but in a productive environment you want to automate and enforce a certain policy.
For CI integrations, cargo deny integrates the RustSec advisory database as well but provides more comprehensive configurations options.
cargo deny helps to check for advisories, verify compatible licenses are used, ban certain crates or limit which sources you allow.
You might want to have a look at the Embark Guideline I mentioned earlier.
A Controlled Approach with Vendoring
A tightly-controlled approach for enterprise settings can be to “vendor” trusted dependencies using cargo vendor.
This creates a local copy of all dependencies in your Rust project(s).
Then, cargo deny can be configured as part of your CI pipeline to only allow the vendored dependencies, and will additionally check for security advisories.
Each new dependency or dependency update will then have to be approved after vetting with the tools described in the previous section.
Conclusion
As I said in the beginning, managing dependencies is hard in any software project, and this is true for Rust as well. When I started looking into the crate ecosystem, I felt I was going deeper and deeper into the rabbit hole. This post is an attempt to structure my findings, and create a snapshot of the current state4.
The bottom line for me is that there is a lot to do to secure the crate ecosystem but there is considerable activity ongoing.
If you want a practical advise here and now, I would try to choose crates that provide the core functionality that you need without adding too many dependencies.
Further, do a background check of the crate on lib.rs, and go for the combination of cargo deny, cargo vet and cargo crev in your CI pipeline which matches your project’s needs.
-
https://github.com/rust-unofficial/awesome-rust/blob/main/CONTRIBUTING.md ↩︎
-
While finishing this write up, I found this excellent talk by Adam Harvey which I can recommend as well: https://www.youtube.com/watch?v=GXkvX9A9xME ↩︎