Join our Newsletter — 33% off our NHI Course
Home› Guides› Rust Security Guide: Memory Safety, unsafe Code and…
Guide Application Security

Rust Security Guide: Memory Safety, unsafe Code and Crate Supply Chain

PDF for newsletter subscribers · free to read online

← All guides
By Lalit Choda, NHI Mgmt Group Updated 24 October 2026 15 min read
On this page

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.rs scripts 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 classSafe RustWhat still applies
Use-after-free, double free, dangling pointers (CWE-416, CWE-415)Prevented by ownership and lifetimesPossible inside unsafe and across FFI
Buffer overflow (CWE-787, CWE-125)Prevented: bounds checks panicget_unchecked, raw pointers and C libraries bypass checks
Data racesPrevented by Send and SyncWrong 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 defaultUse 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 languageParameterised queries, Command::arg rather than a shell, canonicalise and check paths
Denial of service via panic or allocationNot addressedAvoid unwrap() on untrusted input; cap sizes before allocating; set limits in parsers
Authorisation and business logicNot addressedSame design review and testing as any language
Secrets in memory and logsNot addressedAvoid 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 its split_at_mut example.
  • Document invariants: require a // SAFETY: comment on every unsafe block explaining why it is sound, and enforce it with Clippy's undocumented_unsafe_blocks lint.
  • Adopt the 2024 edition: in Rust 2024 the unsafe_op_in_unsafe_fn lint warns by default, so unsafe operations inside an unsafe fn need their own explicit unsafe {} block. cargo fix --edition adds 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 -sys crates 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.

MechanismWhen it runsRiskControl
build.rs in a dependencyEvery build that needs to rebuild the crate, on the hostReads ~/.cargo/credentials.toml, SSH keys, cloud credentials and CI environment variables; network callsReview in cargo-vet audits; build in sandboxed, credential-free jobs
Procedural macrosCompile time, inside the compiler processSame as build scriptsSame; prefer widely reviewed macro crates
IDE language server (rust-analyzer)When a project is opened, to expand macros and run build scriptsOpening an untrusted repository can execute its build codeOpen untrusted code in a disposable environment, or disable build script and proc-macro execution for that workspace
cargo audit without a lockfileWhen generating Cargo.lockThe cargo-audit README warns this can run code from the projectUse cargo audit --file path/to/Cargo.lock from outside untrusted projects
Runtime payload in a dependencyWhen your application or tests runData theft from production or developer machinesVetting, 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 --locked and --offline so 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.

ToolQuestion it answersKey featuresWhere to run it
cargo-auditDo 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.tomlEvery pull request and on a daily schedule
cargo-denyDoes 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-vetHas 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 timeEvery pull request that changes Cargo.lock
Dependabot or equivalentIs a fixed version available?Pull requests from GitHub Advisory Database dataContinuous

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: release setting 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

  1. 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.toml to a recent stable and update it on each six-weekly release.
  2. 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.
  3. 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).
  4. 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.
  5. Harden CI: build with --locked, remove credentials from build jobs, restrict build egress, and move publishing to Trusted Publishing.
  6. Switch to blocking: fail pull requests on new RustSec advisories, new unvetted crates and new undocumented unsafe blocks.
  7. 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

SituationRecommended approachTrade-off
Small team, few dependenciescargo-audit and cargo-deny in CI; forbid(unsafe_code) in application cratesNo review evidence for new crates beyond popularity
Large organisation or regulated productAdd cargo-vet with shared imported audits and an internal audit rota; internal registry mirrorAudit effort; mitigated by delta audits and shared audits
Rewriting C or C++ components in RustPrioritise parsers and network-facing code; keep FFI layers thin and fuzzedMixed codebases keep C-level risk at the boundary
Performance-critical code needing unsafeIsolate in one reviewed crate with Miri and fuzz testsOngoing specialist review cost
Publishing open-source cratesTrusted Publishing with a protected environment; no stored tokensGitHub 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.
MetricTarget
Repositories running cargo-audit and cargo-deny as blocking checks100%
Open RustSec advisories older than 30 days in production binariesZero, or a recorded exception
cargo-vet exemptionsFalling every quarter
Unsafe blocks without a SAFETY commentZero
Crates published via Trusted Publishing100%; zero long-lived publish tokens in CI
Toolchain ageNo repository more than two stable releases behind

Framework mapping

AreaNIST SP 800-53 Rev. 5NIST CSF 2.0ISO/IEC 27001:2022 Annex AMITRE ATT&CK
Memory safety and unsafe reviewSI-16, SA-11PR.PS-06A.8.28Not applicable
Secure development process and toolingSA-15PR.PS-06A.8.25Not applicable
Crate vetting and provenanceSR-3, SR-4ID.RA-01A.8.25T1195.001 Compromise Software Dependencies and Development Tools
Publishing tokens and secretsIA-5PR.PS-06A.8.24T1552.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_log until 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-checks in 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! or include_str!, and keep them out of Debug output.

Standards and references

Related NHI Mgmt Group resources: Memory-Safe Languages Guide · Secure Coding Guide · Software Supply Chain Security Guide · Secrets Management Guide · .NET Security Guide

PDF for subscribers

Take this guide with you

Every section, table and checklist in one PDF, with links to the sources and related guides. Subscribe to our weekly NHI & AI Identity Journal and we’ll email you a download link.

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Written and reviewed by Lalit Choda, NHI Mgmt Group. Last updated 24 October 2026.
NHI Mgmt Group — the #1 independent authority on Non-Human Identity, IAM, and Agentic AI security. nhimg.org

PDF Downloads for Newsletter Subscribers

Get the guide

Enter your details and we’ll email you a download link.

The full guide is free to read online. The PDF is a thank you for subscribing.

Fill in all fields marked * and tick the PDF box to continue.