Rust’s safety reputation hides Cargo build pipeline security risks
Rust stops memory bugs, not malicious build steps. Cargo can run code before your binary exists, so audit the pipeline as carefully as the crate.

Rust’s memory safety story can lull you into missing the real danger zone: the build pipeline. A Cargo project can execute arbitrary code before your binary ever ships, and that means a “safe” language still depends on unsafe assumptions about build scripts, macros, and third-party crates.
The blind spot in a Rust workflow
The sharpest risk is not in your application logic at all. Cargo says a `build.rs` file in the root of a package is compiled and executed just before the package is built, and it receives inputs through environment variables plus the build script’s current directory. That is enough access to read secrets, inspect the filesystem, and behave differently in CI than it does on a laptop.
Procedural macros widen the same trust boundary in a different place. The Rust Reference says they run code at compile time and operate on Rust syntax during macro expansion. In practice, that means a macro can look like ordinary code generation while still seeing environment variables, writing files, or reaching out to external services during compilation.
The key point is simple: memory safety inside your crate does not protect you from code that runs before the crate exists. If a dependency, build script, or macro is hostile, the compiler happily becomes the delivery vehicle.
Why Cargo’s trust model matters
Cargo’s own security policy makes the assumption explicit: user source code and dependencies are treated as trusted and expected to contain no malicious code. That is a useful default for a healthy ecosystem, but it also means compiling or analyzing malicious packages is not treated as a toolchain vulnerability. The toolchain is not built to second-guess every package you ask it to run.
That matters because crates.io does not impose a mandatory review process. The result is an ecosystem that is productive and open, but also exposed to typosquatting, name squatting, and dependency confusion. The September 1, 2023 Rust crates.io malware postmortem showed that this is not hypothetical, and the Rust Foundation Security Initiative said it planned more scanning for typosquatting and malicious crate contents after finding that the uploaded crates had no evidence of real-user downloads.
For everyday Cargo users, this is the practical takeaway: the package registry is part of your attack surface. If you add dependencies casually, you are not just pulling in code, you are extending the set of code that can run during builds, tests, and macro expansion.
What can go wrong in real projects
The threat scenarios are not abstract. A malicious `build.rs` can exfiltrate AWS credentials or other secrets available on a developer machine or CI runner. A typo in a dependency name can pull in an attacker-controlled crate instead of the intended one. A semver range can float to a future patch release that later turns out to be compromised. A proc macro can scan environment variables during macro expansion and quietly ship data out of your build environment.
Cargo’s versioning rules make the semver point especially important. Version requirements such as `0.1.12` are treated as ranges, meaning `>=0.1.12, <0.2.0`. Cargo’s CI guidance also notes that dependency ranges make it useful to test the latest versions because packages generally match version ranges rather than exact pins. That flexibility helps compatibility, but it also means a build can silently resolve to a newer crate without any source change if you do not control the lockfile.
Lockfiles are a security control, not just a convenience
Cargo.lock is one of the simplest defenses you can use, and it is easy to underestimate. Cargo says the lockfile contains exact dependency versions and, when in doubt, should be checked into version control. On August 29, 2023, the Cargo team updated its guidance and said projects should decide based on their own needs, while still calling committing Cargo.lock a reasonable starting point.
For applications and CI pipelines, that advice is hard to ignore. If you do not commit the lockfile, a later build can resolve the same semver ranges to different versions than the ones you tested. That can turn a routine rebuild into an unplanned dependency upgrade, which is exactly the sort of moment an attacker wants. Commit the lockfile, review it like code, and treat lockfile drift as a release event rather than background noise.
A practical defense stack you can use this week
The good news is that Rust already has tools for this layer of the problem, and they fit naturally into Cargo-based workflows.
Start with cargo-audit
RustSec describes its advisory database as the security advisories for crates published through crates.io. `cargo-audit` uses that database to audit Cargo.lock files for known vulnerabilities, so it is a direct fit for the lockfile discipline Rust already recommends. Run it locally, then wire it into CI so regressions never make it to release branches.
Add cargo-deny for policy enforcement
`cargo-deny` goes beyond vulnerability checks. Its documentation says it can check licenses, ban specific crates, detect multiple versions, and inspect advisories and sources. That makes it useful when you want to enforce a team policy, not just catch known CVEs. If you care about avoiding duplicate versions, rejecting risky licenses, or pinning sources, this is where you make those rules visible.
Use cargo-vet for trusted audits
`cargo-vet` is built for a different kind of question: have these dependencies been audited by someone you trust? Its workflow helps projects ensure third-party dependencies have been audited by a trusted entity, and it supports approvals from trusted code owners. Google’s public rust-crate-audits repository shows one large-scale example, with Google saying it uses `cargo-vet` to ensure third-party Rust dependencies have been audited by Googlers or other trusted reviewers.
For teams that want a stronger review story, `cargo-vet` turns “we think this crate is fine” into a documented trust process. That is especially valuable when you depend on widely used crates with a lot of transitive surface area.
How to make build-script risk part of normal Rust hygiene
The easiest mistake is treating build-script and dependency safety as enterprise paranoia. It is not. Cargo’s model lets `build.rs` run arbitrary code during every build, with the same filesystem and network privileges as the developer or CI runner. Procedural macros execute inside compilation itself. Those are normal parts of the Rust workflow, which means the security response needs to be normal too.
A strong baseline looks like this:
- Commit Cargo.lock for applications and CI-driven builds.
- Run `cargo audit` routinely against the lockfile.
- Enforce crate and license policy with `cargo deny`.
- Adopt `cargo vet` where you need trusted dependency review.
- Review every `build.rs` and every procedural macro as build-time code, not just library code.
- Pin private registry sources and keep builds reproducible so a future resolve cannot surprise you.
The larger lesson is that Rust’s safety story is real, but it stops at the compiler boundary. If your workflow trusts every build script, every macro, and every new dependency by default, then the weak point is not memory safety at all. It is supply-chain discipline, and in a Cargo project that discipline belongs in the same checklist as tests, linting, and releases.
Know something we missed? Have a correction or additional information?
Submit a Tip
