Cargo Audit and the Gaps in Rust Dependency Security

php2shell patches (CVE-2026-109618 and CVE-2026-109619) for EOL PHP are out now. With sales replies slower than usual due to high demand, the fastest route to patched packages is to buy ELS for Runtimes online (opens in new tab). Read the patch FAQ

Go straight to an AI summary of this blog post instead

  • Summarize this post with Perplexity
  • Summarize this post with Gemini

Key Takeaways

  • cargo audit only knows about advisories someone already filed.
  • Build scripts and proc macros run arbitrary code at compile time, and stable Cargo can’t switch that off.
  • An abandoned crate has no end-of-life date, so nothing expires and you only get a warning if somebody reports it.
  • When an advisory ships without a patched version, the fix becomes your problem.

A Rust team gets serious about dependency security, and the first move is almost always the same. Install cargo audit, wire it into CI, watch it pass, feel better.

It’s a good move. But cargo audit answers a narrower question than most teams assume. It reads Cargo.lock and tells you which crates have advisories filed against them. That’s not the same as knowing whether the code you’re about to compile is safe to compile.

Rust’s tooling is strong on what the language community prioritized. Reproducible builds, immutable versions, checksummed lockfiles. The gaps aren’t sloppiness. They’re what happens when a toolchain is built for honest dependencies rather than hostile ones.

What cargo audit Actually Checks

Three things, even though the tool gets credited with more.

It checks the crates in your lockfile against the RustSec Advisory Database. It flags yanked versions. And it surfaces informational advisories, which is where Rust does something most package managers skip entirely.

cargo install cargo-audit --locked
cargo audit

RustSec splits its findings into categories, and the three non-vulnerability types carry more practical weight than most teams expect.

Advisory type What it means Fails cargo audit by default
Vulnerability A security flaw with an assigned RUSTSEC ID Yes
Unmaintained The maintainer has stated, or clearly signaled, that they’ve stopped maintaining it No, warning only
Unsound A safe API that can trigger undefined behavior No, warning only
Notice Other advisory-worthy information No, warning only

None of the informational categories fail a run on their own, so a CI job running plain cargo audit goes green with unmaintained crates sitting right there in the tree. That is usually the exact thing teams believe they’re catching.

cargo audit --deny warnings

That changes it. You can also deny a narrower category such as unmaintained, unsound or yanked if you want to promote one class of finding without the rest. Expect the first run on a mature project to be loud.

The unmaintained tag only exists if someone files it. A crate whose maintainer quietly stopped pushing commits in 2023 and never announced anything generates zero advisories. A clean cargo audit run tells you nothing about whether your dependencies still have anyone home.

What Each Rust Security Tool Covers

The tooling splits along two lines that matter more than feature counts: whether it needs a human to have actually reviewed the code, and whether it can stop a build.

Tool Primary check Can fail a build Needs human review
cargo audit Lockfile against RustSec advisories Yes, through its exit status No
cargo deny Advisories, licenses, banned crates, sources Yes, policy-driven No
cargo vet Whether your audit criteria are satisfied Yes, when audits are missing and nothing exempts the crate Yes
cargo crev Distributed web of trust code reviews No Yes
cargo geiger Volume of unsafe code in the tree No No
cargo auditable Embeds the dependency list into the binary No No

cargo deny is the one that’s underused relative to what it actually does. It covers the same RustSec advisory ground as cargo audit, adds license and ban policies, and lets you allowlist sources so a crate from a registry you never approved can’t slip in quietly. Current versions fail the check on vulnerability and notice advisories, default to failing on unmaintained ones anywhere in the tree, and fail on unsound advisories in your direct dependencies, which is the behavior most teams wanted in the first place.

cargo auditable solves a different problem. Once a Rust binary is compiled, recovering its exact dependency list is unreliable. cargo audit bin can partially reconstruct it by parsing panic messages, and its own documentation is refreshingly blunt about the limits. Embedded C code is invisible to it, and roughly half the Rust dependencies go missing because the compiler is very good at removing panics that can never fire. Embedding the real list at build time fixes that, and it costs you one word in your build command.

Run cargo tree --duplicates alongside whatever audit you’re doing. Multiple versions of the same crate in one tree make remediation messier than it looks, because upgrading one dependency path can still leave an older version compiled into your binary.

The First Gap Is Timing

Every tool in that table works from a dependency graph Cargo has already resolved. They can fail a build or block a release. What none of them do is keep a crate out of the resolved graph in the first place.

For most dependency problems, that’s fine. A known-vulnerable crate is still known vulnerable ten minutes later, and catching it in CI is early enough.

For hostile code, it’s the wrong side of the timeline, because Rust executes untrusted code during compilation. Build scripts run as part of cargo build. Procedural macros run inside the compiler. Neither gets a sandbox from Cargo, so both run with the permissions of whoever started the build, reaching the filesystem, the network and any credentials the build environment happens to be holding.

Stable Cargo has no flag to turn either one off.

cargo vendor vendored/
find vendored/ -name build.rs | wc -l

Run that on a real project. A mid-sized service with a few hundred crates in the tree can easily turn up dozens of build scripts, and every one belonging to a crate that actually gets built for your target runs on a clean build. Odds are nobody on the team has read a single one of them.

That’s not hypothetical. In May 2026, researchers documenting the TrapDoor campaign found crates such as sui-framework-helpers and move-compiler-tools aimed at Sui and Move developers. Their build.rs scripts went looking for local wallet keystores the moment a project compiled and shipped what they found to GitHub Gists. No install hook, no import, just cargo build doing exactly what it’s designed to do.

Rust has nothing like npm install --ignore-scripts. You can read a crate’s source before you compile it, and for anything sensitive you should. What stable Cargo won’t do is run a normal build while skipping dependency build scripts and proc macros, so “audit, then compile” stays a manual exercise rather than a flag.

Screening a crate before it reaches a developer, and building it from verified source in a controlled environment, are among the few controls that land earlier than compile-time execution. That timing problem is what SecureChain for Rust is built around, rather than adding another pass over a tree that’s already been fetched.

The Second Gap Is That Nobody Patches Anything

RustSec records vulnerabilities. It doesn’t fix them.

That distinction stays theoretical right up until the advisory in front of you has no patched version. Then it’s the whole problem, and the resolution path narrows fast.

The volume keeps growing, too. RustSec holds 170 advisories carrying 2025 disclosure dates, and advisories dated 2026 passed that total back in May, reaching 257 by mid-August. That’s the fourth year running that the count has gone up, with roughly 160 of this year’s entries filed as vulnerabilities and the rest split between unmaintained and unsound findings. Those counts also creep up after the fact, since advisories for older disclosures keep landing months later. Healthy for the project. Also a growing operational load for anyone consuming the data, because more findings against a tree nobody patches upstream just means more things you can’t close by bumping a version.

Rust also keeps its existing users happy. Developers again ranked it the most admired language in Stack Overflow’s 2025 developer survey, at 72 percent, though admired measures how many current users want to keep using it rather than how fast adoption is climbing. Either way, plenty of shops now run production Rust without any settled practice for what to do about a crate that has an advisory and no fix.

When the patched version doesn’t exist, every option is uncomfortable. You can fork the crate, apply the fix and maintain it under your own name. You can replace the dependency, which usually means rewriting whatever consumed it. You can patch it locally through [patch.crates-io] and own that patch indefinitely. You can document a compensating control and accept the risk. Or you can bring in someone whose actual job is backporting fixes to code upstream left behind.

Backporting a security fix to a version whose maintainer has moved on is a different discipline from writing new features. It takes regression testing, compatibility validation and a deliberate commitment not to change behavior, which is roughly the opposite of what upstream development is built to do.

The Third Gap Is Crates That Never Officially Retire

PHP publishes EOL dates. So does Node, on its own schedule. A crate one person pushed in 2019 publishes nothing, because there was never a support commitment to end in the first place.

The registry at crates.io makes this trickier in one specific way. Published versions are effectively immutable. Deletion only exists in a narrow window, for crates with almost no downloads and nothing depending on them, so yanking is the real lever. That immutability is a genuine security strength next to registries that allow mutable tags. It also means an abandoned crate stays installable, resolves normally, and leaves nothing in Cargo.lock to suggest its maintainer walked away three years ago.

No tool aggregates the signals that would tell you, so this part stays manual. Compare the last release date against the open issue count. Check whether the GitHub repository is archived, which Cargo will never mention. Count the owners listed on the crates.io page, because one owner is a bus factor of one. And note whether the crate is a build dependency or a proc macro, since those execute at compile time no matter how well maintained they are.

Run cargo owner --list <crate> against anything security-relevant in your tree. A single owner on a crate that compiles into your production binary is a supply chain risk that advisory matching alone won’t surface.

Knowing which findings actually matter takes that kind of context. Maintenance status, whether a fix exists, whether the crate sits in your build path or only in dev-dependencies. Advisory matching hands you none of it, so teams end up triaging by RUSTSEC ID instead of by real exposure.

Where cargo vet Helps and Where It Stops

cargo vet takes a different approach, and it deserves more adoption than it’s gotten. Instead of matching advisories, it enforces audit criteria across your dependency graph, using audits your own team performs plus audits you import from organizations that publish theirs.

cargo vet init
cargo vet suggest

Worth knowing before you start: cargo vet init exempts everything already in your tree on purpose, so adoption doesn’t blow up CI on day one. The backlog surfaces through cargo vet suggest, and you trade exemptions for real audits at whatever pace your team can sustain. Importing audits from larger organizations covers a decent chunk of the backlog without anyone on your team reviewing that code themselves.

The limits are structural. Coverage depends on somebody having audited a version you actually depend on, and a new release needs fresh evidence unless an imported audit or an exemption already covers it. Delta audits keep that manageable, since you’re usually reviewing the diff from a version already signed off rather than starting over. A human audit is also a judgment call about code quality and intent, not a guarantee. Reviewers miss things, especially in obfuscated build scripts.

Delegated trust still beats no trust. It just has a coverage curve, and that curve is thinnest exactly where new or obscure crates sit, which happens to be where hostile packages prefer to show up.

What to Actually Run in CI

Pick based on where your project sits rather than installing everything at once.

With nothing in place today, start with cargo deny check advisories sources. It covers the RustSec advisory ground, adds source allowlisting in one step, and treats the core advisory categories as build-breaking errors by default.

If cargo audit is already running, add --deny warnings so unmaintained and unsound findings stop passing silently, then triage the resulting list by whether the crate sits in your build path.

If you ship binaries to customers or into regulated environments, add cargo auditable so the artifact you shipped carries its own dependency list. Reconstructing that afterwards is both unpleasant and incomplete.

If your build agents hold cloud credentials or signing keys, treat compile-time execution as the primary risk and shorten those credential lifetimes, because once a build starts, dependency build scripts can execute before your own code ever does.

And if compile-time execution is unacceptable in your threat model, Cargo alone can’t enforce that boundary. The control has to move upstream of resolution, into screening and rebuilding crates before they reach a build, or into an isolated build environment that assumes the code is hostile.

Your results will vary by tree shape. A project with 40 direct dependencies and no proc-macro-heavy frameworks is a manageable audit. A service pulling in async runtimes, serialization and a web framework can easily reach several hundred transitive crates with plenty of compile-time execution points, and advice that treats those two cases the same is coming from someone who hasn’t run cargo tree on the second one.

Where SecureChain for Rust Fits

Everything above is about finding gaps, not closing them. cargo audit tells you what’s already been flagged. cargo vet tells you what somebody has already reviewed. Neither does anything about a crate that executes malicious code before either tool gets a look at it, and neither produces a fix for an advisory with no upstream patch.

That’s the space SecureChain for Rust is built for. It rebuilds crates from source in isolated environments, screens them for malware and suspicious patterns at install, and keeps patching under SLA as new CVEs land. When upstream maintenance ends, Endless Lifecycle Support picks the same crate up and keeps shipping fixes for as long as it stays in your stack, so an abandoned dependency never quietly becomes the unpatched one in your tree.

If you’re trying to get ahead of what’s buried in your transitive dependencies, TuxCare’s webinar on the dependency iceberg goes deeper into how CVEs propagate through dependency chains that stay invisible even when your dashboard looks clean.

FAQ

What does cargo audit do?

It compares the crates in your Cargo.lock against the RustSec Advisory Database and reports vulnerabilities, yanked versions and informational advisories for unmaintained or unsound crates. It doesn’t read crate source and it doesn’t intercept installs, though its exit status makes it a perfectly good CI gate. An experimental cargo audit fix subcommand can update some vulnerable dependency requirements, but you have to install cargo audit with the fix feature enabled to get it.

Does cargo audit fail on unmaintained crates?

Not by default. Informational advisories, including unmaintained ones, come through as warnings unless you configure otherwise. Run cargo audit --deny warnings to fail on them, or deny a single category such as unmaintained, and expect a long first run on an established project.

What is the difference between cargo audit and cargo vet?

cargo audit checks whether a crate has an advisory filed against it, which asks nothing of you. cargo vet checks whether your dependency graph satisfies audit criteria you configure, using your own audits, imported audits, delta audits and any exemptions you’ve left in place. A new version needs fresh evidence unless something already covers it, and a delta audit usually means reviewing only the diff from a version already audited.

Can Cargo block build scripts from running?

No. Stable Cargo has no supported flag to disable build scripts or procedural macros, both of which execute arbitrary code during compilation with access to whatever the build environment is holding.

How do I find abandoned crates in my dependency tree?

There’s no single command for it. Run cargo audit --deny warnings to catch filed unmaintained advisories, then check last release dates, whether the repository is archived, and owner counts through cargo owner --list for anything security-relevant.

What happens when a RustSec advisory has no patched version?

Nothing resolves it automatically. You fork and fix the crate, replace the dependency, apply a local patch through [patch.crates-io] and maintain it, document a compensating control, or bring in a vendor that backports fixes to crates whose upstream maintainers have stopped maintaining them.

Join 4,500+ Linux & Open Source Professionals!

2x a month. No spam.

Table of Contents
Get the open-source security answers you need

Join 4,500+ Linux & Open Source Professionals!

2x a month. No spam.