Safeguard
Engineering

Rust Crate Security: cargo audit, cargo vet and Beyond

cargo audit catches known-bad versions, cargo vet forces someone to actually read the code. What each tool covers, what neither covers, and how to run both without hating your CI.

Sofia Marchetti
Open Source Program Lead
6 min read

Rust crate security splits into two distinct problems: detecting dependencies with known vulnerabilities, which cargo audit solves by checking Cargo.lock against the RustSec advisory database, and establishing trust in dependencies nobody has reviewed, which cargo vet solves by tracking human audits. Most teams run the first and skip the second, which means they'll catch last year's CVE but not this week's malicious crate. And neither tool addresses the sharpest edge in the ecosystem: build.rs and proc macros execute arbitrary code on your build machine before your tests ever run.

cargo audit: the ten-minute win

Install it pinned, because your security tooling should not itself float:

cargo install cargo-audit --locked
cargo audit

It parses Cargo.lock, matches against rustsec/advisory-db (roughly 700 advisories and growing), and exits non-zero on hits. Advisories carry more structure than most ecosystems get: affected version ranges, patched versions, and often the specific functions involved. cargo audit fix can bump patched versions for you, though I'd treat that as a suggestion generator rather than an auto-merge candidate.

Two flags worth knowing. cargo audit --deny warnings also fails on unmaintained-crate and yanked-version warnings, not just vulnerabilities — stricter than most teams want on day one, right for most teams by month three. And cargo audit bin inspects compiled binaries that were built with cargo auditable, which embeds the dependency list in a dedicated linker section. That last trick is underrated: it means your SBOM travels inside the artifact, and tools like syft can recover the full dependency tree from a bare binary years later. If you're standardizing SBOM handling across ecosystems, SBOM Studio can ingest those alongside your CycloneDX documents.

If you're weighing this against cargo-deny, we've done a head-to-head comparison — short version: cargo-deny adds license policy, banned-crate lists, and source allowlists, and the two coexist fine.

cargo vet: trust as a shared ledger

cargo audit answers "is this version known bad?" cargo vet answers a harder question: "has a human I trust read this code?"

cargo install cargo-vet --locked
cargo vet init
cargo vet

After init, every existing dependency is exempted (recorded in supply-chain/config.toml), and the tool fails when a new or updated crate arrives unvetted. You discharge the failure one of three ways: audit it yourself (cargo vet certify), record a diff audit against a previously-audited version — usually a much smaller read — or import someone else's audits:

# supply-chain/config.toml
[imports.mozilla]
url = "https://raw.githubusercontent.com/mozilla/supply-chain/main/audits.toml"

[imports.google]
url = "https://raw.githubusercontent.com/google/supply-chain/main/audits.toml"

Mozilla and Google publish their audit sets publicly, and between them they cover a large slice of the popular-crate long tail. In a workspace I help maintain, importing both cut our unvetted surface from 212 crates to 61 on day one. The remaining 61 were mostly small transitive utilities — exactly the crates nobody would otherwise ever read, which is rather the point.

The cultural shift matters more than the tooling: cargo vet turns "someone should probably look at new dependencies" into a merge-blocking artifact with named auditors.

build.rs and proc macros: code execution at compile time

A crate with a build.rs runs arbitrary Rust on your machine during cargo build. Proc-macro crates run arbitrary code inside the compiler process. No sandbox, full network access, your CI credentials in the environment. The 2022 rustdecimal incident — a typosquat of rust_decimal that pulled a payload from a remote server when built on CI — used exactly this.

Practical mitigations, in ascending order of effort:

  1. cargo tree --format "package.name" piped through a check for how many of your dependencies even have build scripts; you'll be surprised.
  2. Run builds in ephemeral, network-restricted containers. If build.rs can't reach the internet, most first-stage payloads die quietly.
  3. Scope CI tokens so a compromised build can't push, publish, or read unrelated secrets.
  4. For the genuinely paranoid: separate the cargo fetch step (network on) from cargo build --offline (network off).

Number 2 is the best effort-to-value ratio. It's a CI config change, not a workflow change.

crates.io realities: no namespaces, permanent versions

crates.io has one global flat namespace, so tokio-utils and tokio_utils and tokioutils are all up for grabs by anyone. Typosquatting is the dominant malicious-crate pattern for that reason. Published versions are permanent — yanking hides a version from new resolution but never deletes it, so a Cargo.lock that already references a yanked version keeps building. That's good for reproducibility and means "the author yanked it" is not remediation for a malicious release.

Commit Cargo.lock for binaries and libraries (the old advice to gitignore it for libraries was dropped by the Cargo team in 2023). In CI, build with --locked so a stale or hand-edited lockfile fails loudly instead of being regenerated:

cargo build --locked
cargo test --locked

Lockfile checksums mean any registry-side tampering with an already-locked version fails at download. That, combined with yank-permanence, makes Rust's baseline integrity story better than most ecosystems — the gaps are in new versions and new crates, which is where vet and scanning come in.

Putting it together in CI

A reasonable pipeline, in order:

StageCommandCatches
Resolutioncargo build --lockedLockfile drift, tampered downloads
Known-badcargo audit --deny warningsRustSec advisories, yanked deps
Trustcargo vetUnreviewed new/updated crates
Policycargo deny checkLicenses, banned crates, sources

Total added CI time on a mid-size workspace: about 40 seconds, dominated by the first cargo vet fetch. Continuous SCA scanning — Safeguard covers Cargo alongside the other ecosystems — adds malware-pattern detection and alerting between builds, which matters because malicious crates get discovered on the registry's schedule, not your merge schedule.

Frequently asked questions

Do I need cargo vet if I already run cargo audit?

They answer different questions. cargo audit only knows about published advisories, so a malicious crate that hasn't been reported yet sails through; cargo vet fails closed on anything no trusted auditor has reviewed. Small teams often start with audit-only and add vet once import lists from Mozilla and Google make the initial burden tolerable.

Is a yanked crate version safe to keep using?

Yanking is a signal, not a removal — the code still downloads fine for existing lockfiles. Check why it was yanked (RustSec advisory, soundness bug, accidental publish) before deciding; treat security yanks as a forced upgrade.

How dangerous are build scripts really?

A build.rs is unsandboxed native code execution with your build environment's credentials and network access, triggered by cargo build — before any test runs. Real attacks (rustdecimal, 2022) have used it as the payload stage, so network-restricted build containers are worth the setup cost.

Should Cargo.lock be committed for library crates?

Yes. The Cargo team reversed the old guidance in 2023: committing the lockfile gives contributors and CI reproducible builds and lets --locked act as a tamper check, while downstream consumers still resolve their own graph from your Cargo.toml ranges.

Never miss an update

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