Safeguard
Security Guides

Rust Supply Chain Security: build.rs, Typosquatting, and Auditing crates.io

Rust's borrow checker guarantees memory safety in your code — and nothing about the crates you pull in. A cargo build runs arbitrary code at compile time, before any safe code executes.

Daniel Osei
Security Researcher
7 min read

Rust's borrow checker is a genuine breakthrough: it eliminates the memory-safety bugs that dominate C and C++ CVEs, at compile time, with no runtime cost. It also creates a dangerous intuition — that a Rust project is safe because the language is safe. The borrow checker governs your code. It says nothing about the dozens of crates a real project pulls from crates.io, and it says nothing about what those crates do during the build. A cargo build executes arbitrary code before a single line of your safe code runs, through build.rs scripts and procedural macros. The rustdecimal typosquat (RUSTSEC-2022-0042, part of the "CrateDepression" campaign disclosed in May 2022) exploited exactly this, shipping a crate that mimicked the popular rust_decimal and dropped CI-stealing malware. Cargo itself has shipped advisories around malicious package extraction (CVE-2022-36113 and CVE-2022-36114, fixed in Rust 1.64.0). This guide is about the part of Rust security the compiler does not cover.

Why does cargo build run untrusted code before my code?

Because two mechanisms execute at compile time with your user's privileges. A build.rs build script is compiled and run to generate code, probe the system, or link native libraries — it is ordinary code with full access to your machine. Procedural macros run inside the compiler to transform token streams, again as arbitrary code. Neither is sandboxed:

// build.rs -- this runs on every `cargo build`, before your crate compiles
fn main() {
    // Legitimate uses: codegen, linking native libs.
    // A malicious crate uses the same hook to exfiltrate env vars,
    // read CI secrets, or phone home -- all at build time.
    println!("cargo:rerun-if-changed=build.rs");
}

This is why supply-chain compromise in Rust does not wait until runtime. A poisoned dependency's build.rs runs the moment a developer or CI runner builds the project, which is exactly the vector CrateDepression used to reach cloud build pipelines. Treat adding a dependency as granting it code execution on every machine that builds your project.

How do typosquatting and dependency confusion show up on crates.io?

As crates whose names differ from a popular one by a character or a separator. rustdecimal versus rust_decimal is the canonical case: identical source to the real crate except for one function carrying an obfuscated payload, with fewer than 500 downloads before crates.io removed it. The registry has since added protections, but the defense on your side is discipline: verify a crate's name, repository, and maintainer before adding it, prefer well-established crates with audit history, and be suspicious of a brand-new crate that reproduces a popular one's API.

What tooling audits a Rust dependency tree?

A small, mature set of Cargo subcommands, all worth wiring into CI:

cargo audit    # checks Cargo.lock against the RustSec Advisory Database
cargo deny check   # advisories + license policy + banned/duplicate crates
cargo vet        # verify each dependency has been human-audited (Mozilla)

cargo audit flags known-vulnerable versions from the RustSec database; cargo deny enforces advisory, license, and allow/deny policies in one gate; and cargo vet, from Mozilla, records and enforces that every third-party crate has been reviewed by someone you trust. Commit Cargo.lock for applications and build with --locked in CI so resolution cannot drift, and consider cargo auditable, which embeds the dependency list into the compiled binary so an SBOM can be recovered from the artifact itself.

Does keeping the toolchain current matter?

Yes — the tooling has had its own bugs. CVE-2022-36113 let a malicious package corrupt a file on the extracting machine through a crafted symlink, and CVE-2022-36114 let one exhaust disk space with an archive bomb; both were fixed in Rust 1.64.0. crates.io itself was not exploitable for these because it rejects such packages server-side, but alternate and private registries were exposed, so teams running their own registries especially need a current toolchain. Keep rustc and cargo updated as part of routine maintenance, not as an emergency response.

Rust supply-chain checklist

PracticeWhy it matters
Treat every dependency as build-time code executionbuild.rs/proc-macros run before your code
Verify crate name, repo, and maintainer before addingDefeats typosquats like rustdecimal
Commit Cargo.lock; build with --locked in CIStops silent resolution drift
Run cargo audit against the RustSec database in CIFlags known-vulnerable versions
Add cargo deny for advisory + license + ban policyOne gate for supply-chain policy
Adopt cargo vet for human-audit provenanceEnforces that crates were actually reviewed
Keep rustc/cargo currentFixes tooling CVEs (CVE-2022-36113/36114)

How Safeguard Helps

Safeguard's software composition analysis resolves your full Cargo.lock — every transitive crate, not just direct dependencies — and cross-references the RustSec Advisory Database and CVE feeds against the exact versions you build, with call-path reachability so an advisory your code actually reaches is prioritized over one that never runs. Griffin, our AI analysis engine, inspects new crate releases for the behavioral markers of a compromised or typosquatted package — the build.rs exfiltration pattern CrateDepression used — before a build consumes them. When a safe version exists, auto-fix opens a tested pull request with the bump applied, and developers run the same analysis locally with the Safeguard CLI before pushing. If you are comparing tools, the Safeguard vs Snyk breakdown covers where the reachability model differs. The borrow checker keeps your code memory-safe; Safeguard keeps the crates beneath it honest.

Bring your Rust supply chain under control — start for free or read the documentation.

Frequently Asked Questions

If Rust is memory-safe, why do I need supply-chain security?

Because memory safety and supply-chain safety are different problems. The borrow checker guarantees your code is free of use-after-free and data races, but it does not vet the crates you depend on, and it does not stop a dependency's build.rs or procedural macro from running arbitrary code on your build machine. A safe language with a compromised dependency is still compromised.

What is the actual risk of a build.rs script?

A build script is compiled and executed on every build with the building user's privileges, and it is not sandboxed. A malicious crate uses that hook to read environment variables, steal CI secrets, or contact a remote server — all at build time, before your program ever runs. This is the vector the CrateDepression campaign used against cloud build pipelines, so a new or unfamiliar dependency deserves scrutiny of its build script.

Which Rust auditing tool should I start with?

Start with cargo audit, which checks your Cargo.lock against the RustSec Advisory Database and is the lowest-friction gate to add to CI. Layer cargo deny when you also want license and banned-crate policy in one place, and adopt cargo vet when you are ready to enforce that every dependency has been human-reviewed. Commit Cargo.lock and build with --locked so the audited set is the set you ship.

Were the Cargo CVEs a threat to crates.io users?

Not directly. CVE-2022-36113 and CVE-2022-36114 affected package extraction, but crates.io rejects the malformed packages that trigger them server-side, so its users were not exploitable through this path. The exposure was for alternate and private registries, which is why teams running their own registry should be especially diligent about keeping rustc and cargo updated to 1.64.0 or later.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.