Rust removes whole classes of memory corruption by design, but teams that treat "written in Rust" as "secure" miss where the real risk now sits: unsafe blocks, FFI boundaries, logic bugs and a crate ecosystem in which every dependency can run code on the build machine. Safe Rust's guarantees are strong and well understood; what is less understood is how quickly they erode through unreviewed unsafe, build scripts, procedural macros and long-lived crates.io tokens. This guide covers what the language does and does not guarantee, how to govern unsafe code, how to audit and vet crates with the RustSec tooling and cargo-vet, and how to publish crates without storing tokens. It is for Rust developers, application security engineers and platform teams adopting Rust as part of a memory-safety programme.
Key takeaways
- Safe Rust prevents memory errors, not all bugs: injection, broken authorisation, panics used as denial of service and secret leakage are all still possible.
- Treat every unsafe block as a security review item: keep it small, wrap it in a safe API, document its invariants and test it with Miri.
- Dependencies execute at build time:
build.rsscripts and procedural macros run on the developer or CI machine with the same access as the compiler, before any of your tests do. - Layer three dependency controls: cargo-audit for known vulnerabilities, cargo-deny for policy (sources, licences, bans), and cargo-vet for evidence that someone you trust has reviewed the code.
- Publish without stored tokens: crates.io Trusted Publishing (announced July 2025) exchanges a GitHub Actions OIDC token for a temporary publishing token that the official action revokes when the job ends.
What Rust does and does not guarantee
In safe Rust the borrow checker and type system rule out use-after-free, double free, data races and out-of-bounds access (indexing is bounds-checked and panics rather than reading past the end). That is why government memory-safety guidance, including CISA's "The Case for Memory Safe Roadmaps", points manufacturers towards memory-safe languages. This guide does not repeat that policy case: see the Memory-Safe Languages Guide.
| Bug class | Safe Rust | What still applies |
|---|---|---|
| Use-after-free, double free, dangling pointers (CWE-416, CWE-415) | Prevented by ownership and lifetimes | Possible inside unsafe and across FFI |
| Buffer overflow (CWE-787, CWE-125) | Prevented: bounds checks panic | get_unchecked, raw pointers and C libraries bypass checks |
| Data races | Prevented by Send and Sync | Wrong unsafe impl Send/Sync reintroduces them; deadlocks and logic races are not prevented |
| Integer overflow (CWE-190) | Panics in debug builds; wraps silently in release builds by default | Use checked_*, saturating_* or enable overflow-checks = true in the release profile |
| Injection: SQL, command, path (CWE-89, CWE-78, CWE-22) | Not addressed by the language | Parameterised queries, Command::arg rather than a shell, canonicalise and check paths |
| Denial of service via panic or allocation | Not addressed | Avoid unwrap() on untrusted input; cap sizes before allocating; set limits in parsers |
| Authorisation and business logic | Not addressed | Same design review and testing as any language |
| Secrets in memory and logs | Not addressed | Avoid Debug on secret types; use zeroising wrappers for key material |
Memory safety also does not mean the binary is benign. Rust has become a popular language for malware authors, including infostealers and ransomware, partly because its binaries are harder to analyse with tooling tuned for C and C++. Detection teams should not treat "Rust binary" as a trust signal. See the Infostealer Threat Guide.
Governing unsafe code
The Rust Book lists exactly five operations that unsafe unlocks: dereferencing a raw pointer, calling an unsafe function or method, accessing or modifying a mutable static, implementing an unsafe trait, and accessing fields of a union. It does not switch off the borrow checker. The programmer, not the compiler, becomes responsible for memory validity in those five places, so they are where memory-safety bugs in Rust live.
- Forbid where possible: put
#![forbid(unsafe_code)]at the crate root of application and business-logic crates. Confine necessary unsafe code to a small number of dedicated crates or modules with named owners. - Keep blocks small and wrapped: the Book's guidance is to keep unsafe blocks minimal and expose a safe API whose checks (such as an
assert!on lengths) guarantee the invariants, as in itssplit_at_mutexample. - Document invariants: require a
// SAFETY:comment on every unsafe block explaining why it is sound, and enforce it with Clippy'sundocumented_unsafe_blockslint. - Adopt the 2024 edition: in Rust 2024 the
unsafe_op_in_unsafe_fnlint warns by default, so unsafe operations inside anunsafe fnneed their own explicitunsafe {}block.cargo fix --editionadds them. - Test with Miri: Miri is the official dynamic checker for undefined behaviour (
cargo +nightly miri test). It only finds problems in code that actually executes, so pair it with good test coverage and fuzzing. See the Fuzzing Guide. - Treat FFI as unsafe by default: C libraries reached through
-syscrates carry their own memory-safety bugs. Track them as C dependencies, validate everything crossing the boundary, and patch the native library as well as the crate.
// Wrap unsafe behind a safe, checked API
pub fn read_u32_le(buf: &[u8], offset: usize) -> Option<u32> {
let bytes = buf.get(offset..offset.checked_add(4)?)?; // bounds checked, no unsafe needed
Some(u32::from_le_bytes(bytes.try_into().ok()?))
}
Often the safest unsafe block is the one replaced by a checked standard library call, as above. Review requests to add unsafe by first asking whether a safe equivalent exists.
Build-time code execution: build.rs, proc macros and tooling
Cargo compiles a crate's build.rs into a program and runs it on the host just before building the package. It can compile C code, probe the system, generate code, and do anything else an ordinary program can. The Rust Reference states that procedural macros run during compilation with the same resources as the compiler, including file access, and so "have the same security concerns that Cargo's build scripts have". Both run for every dependency in the graph, not just for your own code.
| Mechanism | When it runs | Risk | Control |
|---|---|---|---|
build.rs in a dependency | Every build that needs to rebuild the crate, on the host | Reads ~/.cargo/credentials.toml, SSH keys, cloud credentials and CI environment variables; network calls | Review in cargo-vet audits; build in sandboxed, credential-free jobs |
| Procedural macros | Compile time, inside the compiler process | Same as build scripts | Same; prefer widely reviewed macro crates |
| IDE language server (rust-analyzer) | When a project is opened, to expand macros and run build scripts | Opening an untrusted repository can execute its build code | Open untrusted code in a disposable environment, or disable build script and proc-macro execution for that workspace |
cargo audit without a lockfile | When generating Cargo.lock | The cargo-audit README warns this can run code from the project | Use cargo audit --file path/to/Cargo.lock from outside untrusted projects |
| Runtime payload in a dependency | When your application or tests run | Data theft from production or developer machines | Vetting, egress controls, monitoring |
- Separate build from release credentials: CI jobs that compile third-party code should hold no publishing tokens, cloud keys or signing keys. Pass artefacts to a separate, minimal publishing job. See the CI/CD Pipeline Identity Security Guide.
- Restrict egress during builds: vendor dependencies (
cargo vendor) or use an internal mirror, then build with--lockedand--offlineso a build script cannot fetch a second stage. - Keep outputs inside
OUT_DIR: Cargo's documentation says build scripts should not modify files outside it. A dependency's script that writes elsewhere is a review finding.
Auditing and vetting crates
The RustSec Advisory Database, maintained by the Rust Secure Code Working Group, records vulnerabilities in crates published on crates.io with IDs in the form RUSTSEC-YYYY-NNNN. Advisories are exported to OSV and imported into the GitHub Advisory Database, which is how Dependabot learns about Rust vulnerabilities. Advisories can also be informational, marked unsound, unmaintained or notice, which matter for long-term risk even without a CVE.
| Tool | Question it answers | Key features | Where to run it |
|---|---|---|---|
| cargo-audit | Do any locked dependencies have known advisories? | Checks Cargo.lock against RustSec; cargo audit bin for binaries (fully accurate when built with cargo auditable); experimental cargo audit fix; ignores via --ignore or audit.toml | Every pull request and on a daily schedule |
| cargo-deny | Does the dependency graph meet our policy? | Four checks: advisories (vulnerable, unmaintained, yanked), bans (specific crates, duplicates), licenses, sources (allowed registries and git URLs) | Every pull request, blocking |
| cargo-vet | Has someone we trust reviewed this exact version? | Audits against criteria; shared audits imported from other organisations; relative (delta) audits between versions; exemptions list reduced over time | Every pull request that changes Cargo.lock |
| Dependabot or equivalent | Is a fixed version available? | Pull requests from GitHub Advisory Database data | Continuous |
cargo-vet criteria
- safe-to-run: the crate can be compiled, run and tested on a workstation or in controlled automation without surprising consequences such as unexpected file access, installing software or contacting untrusted endpoints.
- safe-to-deploy: the crate will not introduce a serious vulnerability into production software exposed to untrusted input. Auditors must reason about all unsafe blocks and powerful imports. It implies safe-to-run.
- Use both: require safe-to-run for development and build dependencies, and safe-to-deploy for anything linked into the shipped binary.
Advisory scanning only catches what has already been reported. The faster_log and async_println crates removed from crates.io on 24 September 2025 were typosquats of real logging crates, with 7,181 and 1,243 downloads, whose runtime code searched log files for Ethereum and Solana private keys and sent them to a remote server. They had sat on the registry since 25 May 2025. Name checks in cargo-deny's bans and review through cargo-vet are what catch that class. See the Malicious Package and Dependency Confusion Guide.
Publishing crates: tokens and Trusted Publishing
A crates.io API token with publish scope lets its holder release a new version of every crate it covers. Cargo stores tokens used by cargo login in $CARGO_HOME/credentials.toml in plain text, which makes developer laptops and CI caches attractive targets, as registry worms in other ecosystems have shown. In the Shai-Hulud npm worm of September 2025, every stolen publishing token was used to trojanise the owner's other packages, and trusted publishing was one of the main recommended defences.
The crates.io team announced Trusted Publishing in its July 2025 development update, implementing RFC 3691. It currently supports GitHub Actions. The first release of a crate must be published manually; after that, a trusted repository and workflow are configured on crates.io and the workflow exchanges a GitHub OIDC token for a short-lived crates.io token. The rust-lang/crates-io-auth-action post step revokes the token automatically when the job completes.
jobs:
publish:
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: rust-lang/crates-io-auth-action@v1
id: auth
- run: cargo publish
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
- Use a protected environment: the crates.io update describes the optional
environment: releasesetting as adding security; require reviewers on that environment. - Delete legacy tokens: after the first trusted release, revoke API tokens stored in CI secrets and on maintainers' machines.
- Scope what remains: any token you must keep should be limited to specific crates and the publish-update scope, with an expiry date.
- Protect maintainer accounts: crates.io signs in through GitHub, so the maintainer's GitHub account security (phishing-resistant MFA, no stale OAuth grants) is the root of trust. See the Software Supply Chain Security Guide.
Where identity fits
Rust services handle the same non-human credentials as any other: database passwords, cloud keys, API tokens and TLS private keys. Load them at runtime from a secrets manager or workload identity rather than embedding them with include_str! or env!, both of which bake the value into the compiled binary where anyone with the artefact can extract it. Wrap secrets in types that do not implement Debug or Display so they cannot be logged by accident, and zero key material on drop. See the Secrets Management Guide and the Workload Identity Federation Guide.
Rolling out a Rust security programme
- Baseline: list Rust repositories, their editions and toolchain channels. The Rust project ships security fixes only for the latest stable release (plus beta and nightly), so pin
rust-toolchain.tomlto a recent stable and update it on each six-weekly release. - Measure unsafe: count unsafe blocks per crate in your own code and in dependencies (for example with cargo-geiger) to find where review effort belongs.
- Add cargo-audit and cargo-deny in warning mode on every pull request; review findings for two sprints and write the initial
deny.toml(allowed registries, banned crates, licence list). - Initialise cargo-vet: run
cargo vet init, which records current dependencies as exemptions, import audits from organisations you trust, and set a target for shrinking the exemptions list each quarter. - Harden CI: build with
--locked, remove credentials from build jobs, restrict build egress, and move publishing to Trusted Publishing. - Switch to blocking: fail pull requests on new RustSec advisories, new unvetted crates and new undocumented unsafe blocks.
- Rollback: all controls are configuration files in the repository (
deny.toml,supply-chain/, CI workflow), so a team can revert to warning mode per repository if a gate blocks an urgent fix; record the exception and its expiry.
Decision guidance
| Situation | Recommended approach | Trade-off |
|---|---|---|
| Small team, few dependencies | cargo-audit and cargo-deny in CI; forbid(unsafe_code) in application crates | No review evidence for new crates beyond popularity |
| Large organisation or regulated product | Add cargo-vet with shared imported audits and an internal audit rota; internal registry mirror | Audit effort; mitigated by delta audits and shared audits |
| Rewriting C or C++ components in Rust | Prioritise parsers and network-facing code; keep FFI layers thin and fuzzed | Mixed codebases keep C-level risk at the boundary |
| Performance-critical code needing unsafe | Isolate in one reviewed crate with Miri and fuzz tests | Ongoing specialist review cost |
| Publishing open-source crates | Trusted Publishing with a protected environment; no stored tokens | GitHub Actions only today |
Detection and measurement
- Dependency changes: alert on pull requests that add a crate with a build script or proc-macro, a git or non-crates.io source, or a name close to an existing dependency.
- CI behaviour: monitor build jobs for outbound connections to unexpected hosts and for reads of credential files.
- Registry activity: watch for new versions of your crates published outside the trusted workflow, and for new API tokens created on maintainer accounts.
| Metric | Target |
|---|---|
| Repositories running cargo-audit and cargo-deny as blocking checks | 100% |
| Open RustSec advisories older than 30 days in production binaries | Zero, or a recorded exception |
| cargo-vet exemptions | Falling every quarter |
| Unsafe blocks without a SAFETY comment | Zero |
| Crates published via Trusted Publishing | 100%; zero long-lived publish tokens in CI |
| Toolchain age | No repository more than two stable releases behind |
Framework mapping
| Area | NIST SP 800-53 Rev. 5 | NIST CSF 2.0 | ISO/IEC 27001:2022 Annex A | MITRE ATT&CK |
|---|---|---|---|---|
| Memory safety and unsafe review | SI-16, SA-11 | PR.PS-06 | A.8.28 | Not applicable |
| Secure development process and tooling | SA-15 | PR.PS-06 | A.8.25 | Not applicable |
| Crate vetting and provenance | SR-3, SR-4 | ID.RA-01 | A.8.25 | T1195.001 Compromise Software Dependencies and Development Tools |
| Publishing tokens and secrets | IA-5 | PR.PS-06 | A.8.24 | T1552.001 Credentials In Files; T1195.002 Compromise Software Supply Chain |
In CIS Controls v8.1 this sits mainly in Control 16 (Application Software Security). See the Secure Coding Guide.
Common failure patterns
- "It's Rust, so it's safe": skipping threat modelling and injection testing because the language is memory safe, leaving SQL, command and path traversal bugs untouched.
- Unsafe sprawl: unsafe blocks added for convenience or performance across many modules, without SAFETY comments or Miri, so no one can say which invariants the code relies on.
- Release builds that wrap: length or size arithmetic that panics in debug testing but silently wraps in release, feeding a wrong value into an unsafe block or an allocation.
- Credentials in build jobs: the same CI job compiles every dependency's build script and holds the crates.io token and cloud keys, so one malicious crate can steal them all.
- Audit without vetting: relying on cargo-audit alone, which could not have flagged typosquats such as
faster_loguntil after they were reported. - Secrets compiled in: API keys pulled in with
env!at build time, so every released binary and container layer carries them.
Practitioner checklist
- Add
#![forbid(unsafe_code)]to application crates and isolate remaining unsafe in owned modules. - Require a SAFETY comment on every unsafe block and run Miri and fuzz tests on that code.
- Adopt the 2024 edition and keep the toolchain on a current stable release.
- Enable
overflow-checksin release or use checked arithmetic for sizes and lengths. - Run cargo-audit and cargo-deny as blocking checks on every pull request and daily.
- Introduce cargo-vet and reduce exemptions each quarter, with safe-to-deploy for shipped code.
- Build third-party code in credential-free, egress-restricted jobs with
--locked. - Move crates.io publishing to Trusted Publishing with a protected environment and revoke old tokens.
- Load secrets at runtime, never through
env!orinclude_str!, and keep them out ofDebugoutput.
Standards and references
- The Rust Programming Language: Unsafe Rust
- The Rust Reference: Procedural Macros
- The Cargo Book: Build Scripts
- Rust Secure Code Working Group: RustSec Advisory Database
- Mozilla: cargo-vet Built-in Criteria
- Rust Blog: crates.io Development Update (Trusted Publishing)
- Rust Blog: crates.io Malicious Crates faster_log and async_println
- CISA: The Case for Memory Safe Roadmaps
Related NHI Mgmt Group resources: Memory-Safe Languages Guide · Secure Coding Guide · Software Supply Chain Security Guide · Secrets Management Guide · .NET Security Guide