Skip to content

About

Where Rust's safety guarantees stop, and how to secure Rust code, dependencies, builds and services. Every claim sourced, every example compiled in CI.

Topics

Resources

Stars

20 stars

Watchers

1 watching

Forks

Latest commit

 

History

18 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Rust for Security and Privacy Researchers

Where Rust's safety guarantees stop, and how to secure Rust code, dependencies, builds and services. CI compiles every Rust example below.

Last verified 2026-10-05 · Rust 1.99 · edition 2024

Job Go to First action
Know what Rust prevents Guarantees Plan for panics, overflow, logic bugs (LIM-2–LIM-4)
Vet a dependency Dependencies Check its crates.io Security tab (DEP-1)
Secure builds, releases, incident response Supply chain Commit Cargo.lock (SUP-4); advisory: SUP-8
Write or audit unsafe, FFI (foreign-code calls) Unsafe Check each // SAFETY: against the callee's # Safety docs (UNS-4)
Share data across threads or tasks Concurrency thread::scope or Arc<Mutex<T>> (CON-1)
Audit unfamiliar code Auditing Open it only where no credentials live (SUP-5, AUD-9)
Choose crypto, TLS, secret storage Crypto rustls ≥0.23.45 (CRY-1)
Harden a network service Services Time out slow clients (SVC-1)
Pick a bug-finding tool Tools Run tests reaching unsafe under Miri (undefined-behavior detector) (TOOL-1)
Harden or sandbox a binary Hardening Release overflow checks (HARD-1)
Embedded, kernel, Wasm, safety-critical Domains Guard firmware stacks (DOM-1)
Zero-knowledge (ZK) proofs, threshold signing, FHE (encrypted computation) Privacy tech Test that circuits reject tampered values (PRIV-1)
Learn from past incidents Casebook Update Rust promptly (CASE-1)
Fix code from this guide's 2024 version Corrections Apply each row's fix
Report or follow vulnerabilities Stay current Toolchain: security@rust-lang.org; crates: SUP-9

If you do only ten things

  1. Commit Cargo.lock and build with --locked (SUP-4).
  2. Fail CI on RustSec advisories (DEP-1) and update Rust promptly (CASE-1).
  3. Build or open untrusted code only where no credentials live (SUP-5).
  4. Never unwrap or expect input or config (LIM-2).
  5. Give each unsafe block a // SAFETY: proof (UNS-4) and run its tests under Miri (TOOL-1).
  6. Use rustls ≥0.23.45 and never disable certificate checks (CRY-1).
  7. Load secrets from a secret store (a file your orchestrator mounts, or a secrets manager) into SecretString (CRY-9).
  8. Bound every request's time and size (SVC-1, SVC-4).
  9. Fuzz every parser of untrusted input (TOOL-3).
  10. Use the lints and release profile of this repository's snippets crate (UNS-1, TOOL-4, HARD-1):
# Workspace: put all of this in the root Cargo.toml, the lints as [workspace.lints.rust] and
# [workspace.lints.clippy]; add [lints] workspace = true to every package (never a virtual root).
[lints.rust]
unsafe_code = "forbid" # UNS-1. Crates that need unsafe: "deny", then #[allow] per module

[lints.clippy] # Opt-in restriction lints (TOOL-4); justify each #[expect(lint, reason = "...")]
# Only Clippy checks these, so run `cargo clippy --all-targets --locked` in CI. For tests, set
# allow-unwrap-in-tests, allow-expect-in-tests, allow-indexing-slicing-in-tests in clippy.toml
# Existing code: start indexing_slicing, arithmetic_side_effects at "warn"; #![deny] per module
unwrap_used = "deny"                # LIM-2
expect_used = "deny"                # LIM-2
indexing_slicing = "deny"           # LIM-2
arithmetic_side_effects = "deny"    # LIM-3
as_conversions = "deny"             # AUD-3
undocumented_unsafe_blocks = "deny" # UNS-4

[profile.release]
# HARD-1: overflow panics instead of wrapping, in dependencies too
# (`as` casts still truncate silently)
overflow-checks = true
# HARD-2: the default; with "abort", any panic ends the whole process
# and catch_unwind cannot stop it. A panic while holding a std Mutex
# poisons it: handle PoisonError instead of unwrapping lock().
panic = "unwind"
# HARD-3: strip removes symbol names but is no meaningful security measure (rustc docs), and
# "symbols" can garble backtraces: drop it if you need crash reports. Panic messages keep
# source paths, so remap them in CI release builds (the last matching prefix wins; add one
# for a CARGO_HOME outside HOME). RUSTFLAGS replaces every rustflags setting in
# .cargo/config.toml; if you have any, add the flag there with an absolute path instead:
# RUSTFLAGS="--remap-path-prefix=$HOME=~" cargo build --release --locked
strip = "symbols"

Corrected since the 2024 version

Old advice Risk Fix
"Pin dependencies" with = Blocks semver-compatible fixes Commit Cargo.lock, build --locked (SUP-4)
Token from OsRng and % len, then printed Breaks on rand ≥0.9; biased; leaked getrandom::fill (CRY-6)
Secrets in env vars; default database fallback; URL printed Fails open; leaks credentials Secret store; fail closed (CRY-9)
rustls with_safe_defaults and webpki Pre-0.22 API; verifier idle since 2024-02 rustls ≥0.23.45, platform verifier (CRY-1)
"Implement secure communication protocols" like TLS, SSH Home-made protocol bugs rustls, russh or snow (Crypto)
JWT with a hard-coded key, no backend, no iss/aud check, token printed Forgery; panics; leaked tokens CRY-2
"type safety extend to FFI boundaries"; bindgen run from main; to_str().unwrap() on C strings Unchecked signatures; panics; wrong-allocator frees UNS-10, UNS-12
Raw pointers via &num, then &mut num &mut invalidates the first (Miri, Stacked Borrows) &raw const, &raw mut (UNS-5)
"Arc is both Send and Sync"; threads never joined Unsound sharing; lost work CON-1
Redaction credited to "move semantics" Secret never wiped SecretString (CRY-9)
thiserror {0} in client errors Leaks internals SVC-14
Rust's types "prevent common web vulnerabilities" like XSS Unescaped output Autoescaping templates, DOM-3
rust-analyzer for "vulnerability detection" Runs untrusted build code; finds no vulnerabilities AUD-9
cargo audit for crypto misuse; RustCrypto "audits and formal verification" False assurance Check audit scope (Crypto)
Groth16 with a single-party setup; Paillier as MPC; GG18/GG20 code Forged proofs; key extraction PRIV-2, PRIV-5
PQClean for post-quantum Archived CRY-8
Fuzzing std parsing Tests std, not your code TOOL-3
Prusti as the leading verifier Last release 2024-03 TOOL-6, TOOL-7
A libssh2 authentication bypass Rust "would have prevented" Overstates Rust That was libssh CVE-2018-10933, a logic bug (LIM-4)
Firecracker's and Tock's isolation credited to Rust Ignores other layers KVM, seccomp, jailer (Hardening); MPU (Domains)

What Rust does and doesn't protect

Safe Rust (no unsafe) prevents memory corruption and data races: Android estimates, from one near-miss, 0.2 vs ~1,000 memory-safety vulnerabilities per million lines, Rust vs C/C++. Go, Java and Python are memory-safe too, but racy Go can corrupt memory. Both guarantees need sound rustc, std and unsafe (no undefined behavior), so safe Rust is no sandbox.

Property Holds only if Real failure Control
Memory and data-race safety rustc, std and all unsafe are sound cve-rs; Linux Binder LIM-1 Run untrusted Rust (plugins, generated code) as Wasm (HARD-5) or in a separate OS-sandboxed process (HARD-4)
No crash, leak, deadlock never: unwrap, expect, [i] panic; safe Rust allows leaks, deadlocks Cloudflare outage LIM-2 Never unwrap or expect input or config; CON-3
No integer overflow never: release wraps Tock kernel crash LIM-3 Use checked_* arithmetic on input sizes (HARD-1)
Correct logic never sudo-rs authentication bypass LIM-4 Test that authentication rejects bad cases
No TOCTOU (check-then-use race) never std, uutils, sudo-rs CON-6
Constant time never: LLVM may add branches curve25519-dalek CRY-5, CRY-7
Safe builds every build.rs, proc-macro is benign arrayref SUP-5

Legend. Boundary diagrams: subgraph = trust zone; solid arrow = compiler-checked (types only); dashed = trusted, unchecked. Decision diagram: diamond = question; rectangle = action; arrow label = answer; ✗ AVOID = stop.

Unlabeled arrows mean "relies on": rustc can't verify unsafe promises; any crate's build code runs on your machines.

flowchart TD
  accTitle: Where Rust's guarantees stop
  accDescr: Safe code in any crate is type-checked by rustc and calls your unsafe blocks through a safe API. Below that, each layer is trusted without checks - dependencies' unsafe code, std's unsafe code, rustc and LLVM, then the FFI and C libraries your binary links, then the operating system. Any crate, even one with no unsafe code, can ship build scripts and proc-macros that run with your privileges on developer and CI machines.
  subgraph z1["Checked by rustc"]
    n1["Safe code, any crate"]
  end
  subgraph z2["Trusted: soundness unchecked"]
    n2["Your unsafe blocks"]
    n3["Dependencies' unsafe code"]
    n4["std's unsafe code"]
    n5["rustc and LLVM"]
  end
  subgraph z3["Outside Rust"]
    n6["FFI and C libraries"]
    n7["Operating system"]
  end
  subgraph z4["Build time"]
    n8["build.rs and proc-macros"]
    n9["Developer and CI machines"]
  end
  n1 -->|"calls via safe API"| n2
  n2 -.-> n3
  n3 -.-> n4
  n4 -.-> n5
  n5 -.->|"your binary links"| n6
  n6 -.-> n7
  n1 -.->|"any crate may ship"| n8
  n8 -.->|"run with your privileges"| n9
Loading

Go deeper

Is this crate safe to depend on?

A dependency's build script (build.rs) and procedural macros (compile-time functions) run arbitrary code when you build (Cargo, Reference), and the toolchain trusts them fully. Vet new crates before their first build:

flowchart TD
    accTitle: Crate vetting decision flow
    accDescr: Each question is answered by the check with the same ID below. An unpatched advisory, archived repository or deprecation (DEP-1) means avoid, or vendor a patched copy. A crate with build-time code (DEP-2), no activity in the past year (DEP-3), or many or unknown publishers (DEP-4) must be covered by imported cargo-vet audits (DEP-5) or by your own audit. Otherwise depend on it. A failed audit means avoid, or vendor a patched copy.
    Q1{"DEP-1: unpatched advisory, archived, deprecated?"} -->|yes| X["✗ AVOID, or vendor patched"]
    Q1 -->|no| Q2{"DEP-2: build.rs or proc-macro?"}
    Q2 -->|no| Q3{"DEP-3: active in past year?"}
    Q2 -->|yes| Q5{"DEP-5: cargo-vet audits cover it?"}
    Q3 -->|no| Q5
    Q3 -->|yes| Q4{"DEP-4: how many can publish?"}
    Q4 -->|"many or unknown"| Q5
    Q4 -->|"few, known to you"| D["Depend"]
    Q5 -->|yes| D
    Q5 -->|no| A["Audit, then depend"]
    A -->|fails| X
Loading
  • DEP-1 Before adding a crate, confirm its exact name (typosquats), then check its crates.io Security tab for unpatched advisories. After, cargo deny check advisories (0.20.2, 2026-07-09) fails on vulnerable or unmaintained crates; add unsound = "all" for transitive unsoundness. RustSec misses some repository-only advisories (cap-std). Archived or x.y.z+deprecated also means avoid: RustSec has no unmaintained advisory for archived serde_yaml.
  • DEP-2 Audit uncovered build-time code in the crates.io Code tab, which shows what Cargo downloads, not the repository. Stop rust-analyzer first; it runs that code.
  • DEP-3 Activity is the later of the last release and default-branch commit.
  • DEP-4 cargo supply-chain publishers (0.3.7, 2026-02-05) lists who can publish your dependencies; any can ship malware.
  • DEP-5 Run cargo-vet (0.10.2, 2026-01-13) in CI so new crates and versions fail until audited. Imports are not transitive; Google calls its audits best-effort. Vendor a patched copy via [patch].
cargo install --locked cargo-deny@0.20.2 cargo-supply-chain@0.3.7 cargo-vet@0.10.2
cargo vet init              # once per project
cargo vet import google
cargo vet import mozilla
gh api repos/OWNER/REPO/security-advisories --jq '.[] | .ghsa_id + " " + .summary' # DEP-1
gh api repos/OWNER/REPO --jq .archived # DEP-1: true means avoid
cargo add CRATE             # stop rust-analyzer first (DEP-2)
cargo deny check advisories # DEP-1
cargo metadata --locked --format-version 1 | jq -r '.packages[]
  | select(any(.targets[].kind[]; . == "custom-build" or . == "proc-macro"))
  | "\(.name) \(.version)"'  # DEP-2
gh api 'repos/OWNER/REPO/commits?per_page=1' --jq '.[0].commit.committer.date' # DEP-3
cargo supply-chain publishers # DEP-4
cargo vet trust CRATE PUBLISHER # DEP-4: only for few, known publishers
cargo vet                   # DEP-5: lists what still needs an audit

Go deeper

Supply chain, releases, response

Attacks enter at every stage. Commit Cargo.lock, isolate secrets, publish without stored tokens, update for advisories, report privately.

flowchart TD
    accTitle: Supply-chain path
    accDescr: Seven stages in three trust zones, each labeled with the control in the table below. Publishing, for your crates and your dependencies, holds the maintainer account (SUP-1), the publish step (SUP-2) and the registry (SUP-3). Your build holds Cargo.lock (SUP-4), build scripts and proc macros (SUP-5), and CI secrets and caches (SUP-6). Users receive the shipped binary (SUP-7). Every edge is dashed because nothing on this path is compiler-checked.
    subgraph UP["Publishing"]
        A["SUP-1: Maintainer account"] -.-> B["SUP-2: Publish"]
        B -.-> C["SUP-3: Registry"]
    end
    subgraph YB["Your build"]
        D["SUP-4: Cargo.lock"] -.-> E["SUP-5: build.rs, proc macros"]
        E -.-> F["SUP-6: CI secrets, caches"]
    end
    subgraph US["Users"]
        G["SUP-7: Shipped binary"]
    end
    UP -.-> YB
    YB -.-> US
Loading
Stage Control Tool
Maintainer account (phishing) SUP-1 Sign in only via typed or bookmarked URLs (2025-09-12); schedule calls yourself; never run what callers send (2026-09-17). Passkeys
Publish SUP-2 Use Trusted Publishing (30-minute CI tokens), then disable token publishing (2026-01-21). crates-io-auth-action v1.0.5
Registry (tar) SUP-3 Run Rust ≥1.96.1 (Cargo's SSH-library CVEs). Check advisories daily (malware too). DEP-1, OSS Rebuild
Cargo.lock (arrayref) SUP-4 Commit Cargo.lock; build, install --locked; keep caret requirements, not = pins (Cargo). Dependabot (3-day cooldown) or Renovate minimumReleaseAge
build.rs, proc macros (proc-macro1) SUP-5 Building or opening a repo runs its code; hold no credentials there. Container or VM
CI secrets, caches (Miri) SUP-6 No secrets in jobs that write caches PRs can read (2026-09-21). GitHub environments, zizmor 1.30.1
Shipped binary SUP-7 Use cargo auditable build; attach an SBOM (dependency list), provenance (signed build record). cargo-auditable 0.7.7, cargo-cyclonedx 0.5.9, actions/attest v4.2.2

Respond:

  • SUP-8 Advisory in your tree: cargo update <crate>; if a parent blocks it (cargo tree -i <crate>), upgrade the parent or [patch] a fixed fork. Malware: wipe build machines; rotate secrets from clean ones (2022-05-10).
  • SUP-9 Toolchain or crates.io bug: security@rust-lang.org (policy). Crate bug: owners privately, then GitHub and RustSec advisories. Malware: help@crates.io (policy).
  • SUP-10 Your crate: revoke leaked credentials first; ship semver-compatible fixes, then advisories; yanks miss lockfiles (Cargo).
  • SUP-11 Manufacturer selling in the EU: since 2026-09-11, report actively exploited vulnerabilities, severe incidents via CRA's Single Reporting Platform: 24 h early warning, 72 h notification.

SUP-2 on GitHub:

# .github/workflows/release.yml. Trusted Publishing needs the crate on crates.io already: publish
# the first version with an API token scoped to publish-new and this crate, with an expiry; then
# register this file name and the "release" environment on crates.io and disable token publishing.
on: { push: { tags: ["v*"] } }
permissions: {}
jobs:
  verify: # builds the crate, so dependency build scripts run here, without credentials
    runs-on: ubuntu-latest
    permissions: { contents: read }
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with: { persist-credentials: false }
      - run: cargo package --locked
  publish: # any step here can mint an OIDC token, so build nothing (--no-verify) and restore no caches
    needs: verify
    runs-on: ubuntu-latest
    environment: release # in repo settings: require a reviewer, allow only v* tags
    permissions: { contents: read, id-token: write }
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with: { persist-credentials: false }
      - uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
        id: auth
      - run: cargo publish --locked --no-verify
        env: { CARGO_REGISTRY_TOKEN: "${{ steps.auth.outputs.token }}" }

Go deeper

Unsafe and FFI

unsafe and FFI rest on promises the compiler can't check; breaking one is undefined behavior (UB) program-wide.

Check Enforce with
UNS-1 Forbid unsafe where unneeded; dependencies and their macros' unsafe aren't covered. [lints.rust] unsafe_code = "forbid" (Cargo ≥1.74)
UNS-2 Edition 2024 makes env::set_var/remove_var unsafe: call them in non-async main before spawning threads. edition = "2024" (Rust ≥1.85)
UNS-3 Prefer safe APIs: derived casts over transmute. zerocopy 0.8.59, bytemuck 1.25.2
UNS-4 Give each block one unsafe operation and a // SAFETY: proof of every # Safety precondition, valid while other threads run. Clippy undocumented_unsafe_blocks, multiple_unsafe_ops_per_block
UNS-5 Keep unsafe small, inside a safe wrapper sound for every input; use &raw const/&raw mut. Clippy borrow_as_ptr; deny unsafe_op_in_unsafe_fn
UNS-6 Check length math, distrust safe traits, stay panic-safe; Drop may never run. Clippy arithmetic_side_effects; UNS-8
UNS-7 Bound unsafe impl Send/Sync like std: Box<T> on T: Send/Sync; Arc<T> on T: Send + Sync; Mutex<T> on T: Send. AUD-1
UNS-8 Run every test reaching unsafe under Miri (AddressSanitizer for FFI); fuzz safe wrappers. TOOL-1, TOOL-3
UNS-9 Model-check loop-bounded unsafe functions; Kani misses aliasing. TOOL-6
UNS-10 Generate unsafe extern signatures; Rust trusts them unchecked. Take C enums, bools as integers; validate. bindgen 0.73.2 in build.rs, allowlisted
UNS-11 Catch panics before they reach C (abort since 1.81). catch_unwind; extern "C-unwind" (1.71) for intended unwinding
UNS-12 Free memory with its allocator. A Drop wrapper
Allocated by Freed by Rust type or API Failure if wrong
Rust (CString::into_raw) Rust CString::from_raw C free(): wrong allocator
Rust (Box::into_raw) Rust, once Box::from_raw Double free
C, lent to you C CStr::from_ptr, then copy Use after free, double free
C, given to you Its library's free Drop wrapper CString::from_raw/Box::from_raw: UB
use core::ffi::{CStr, c_char, c_int, c_void};

// Rust trusts these signatures unchecked: generate them from the header (UNS-10).
unsafe extern "C" {
    fn abs(x: c_int) -> c_int; // keep unsafe, never `safe fn`: abs(INT_MIN) is UB
    fn strdup(s: *const c_char) -> *mut c_char; // caller frees with free()
    fn free(p: *mut c_void);
}

fn main() {
    // SAFETY: -42 != c_int::MIN, so abs is defined.
    let y = unsafe { abs(-42) };
    // SAFETY: a c"..." literal is NUL-terminated and 'static.
    let p = unsafe { strdup(c"from C".as_ptr()) };
    if p.is_null() {
        return; // allocation failed
    }
    // SAFETY: p is non-null and NUL-terminated; we copy it out before free().
    let s = unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned(); // no unwrap
    // C allocated p, so C frees it. In a library, free in a Drop impl (UNS-12).
    // SAFETY: p came from strdup and is freed exactly once, here.
    unsafe { free(p.cast()) };
    println!("{y} {s}");
}

Go deeper

Concurrency and async

Safe Rust rejects data races (unsynchronized concurrent access, one writing) at compile time, not race conditions or deadlocks (Nomicon); unsafe races can corrupt memory. Async adds cancellation, unbounded queues, blocked executors.

Need Use Avoid (failure)
CON-1 Cross-thread sharing thread::scope, or Arc of a Mutex, RwLock or atomic (Arc<T>: Send needs T: Send + Sync) Unconstrained unsafe impl Send/Sync (UNS-7)
CON-2 Panic while locked Fail closed, or re-check invariants, then PoisonError::into_inner .lock().unwrap() in servers (panics cascade); tokio::sync::Mutex (silently unlocks)
CON-3 Deadlocks One lock order; drop std guards before .await (await_holding_lock) Relocking a held Mutex (hang/panic)
CON-4 Cancellation: select!, timeout, client disconnects drop futures reserve(), then Permit::send (tokio ≥1.53.2; 1.51.x: ≥1.51.5) send, read_exact, write_all (lost data)
CON-5 Bound queues, blocking mpsc::channel(n) (threads: sync_channel(n)); Semaphore-capped spawn_blocking unbounded_channel, std channel (unbounded memory); blocking in tasks (stall)
CON-6 File check-then-use (TOCTOU) One opened handle (cap-std ≥4.0.3) Re-resolving the path (symlink swap)
CON-7 Lock-dependent unsafe // SAFETY: holds in all interleavings; loom, shuttle Single-thread reasoning (memory corruption)
use tokio::{sync::mpsc::{self, Sender}, time::{Duration, timeout}};

#[derive(Debug, PartialEq)]
enum Fwd { Full(String), Closed(String) } // either way, the caller still owns the message

// CON-4: reserve a slot before handing over `msg`, so a timeout can't lose it.
async fn forward(tx: &Sender<String>, msg: String) -> Result<(), Fwd> {
    match timeout(Duration::from_secs(1), tx.reserve()).await {
        Ok(Ok(permit)) => { permit.send(msg); Ok(()) }
        Ok(Err(_closed)) => Err(Fwd::Closed(msg)), // receiver gone for good: never retry
        Err(_elapsed) => Err(Fwd::Full(msg)),      // queue full: retry later or reject
    }
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let (tx, mut rx) = mpsc::channel(1); // bounded: back-pressure, not memory growth
    assert_eq!(forward(&tx, "a".into()).await, Ok(()));
    assert_eq!(forward(&tx, "b".into()).await, Err(Fwd::Full("b".into()))); // "b" comes back
    assert_eq!(rx.recv().await.as_deref(), Some("a"));
    drop(rx); // consumer gone: fail at once instead of waiting out the timeout
    assert_eq!(forward(&tx, "c".into()).await, Err(Fwd::Closed("c".into())));
}

Go deeper

Auditing Rust code

Confirm grep hits in a SUP-5 sandbox: Clippy, Miri, CodeQL and rust-analyzer (which finds no vulnerabilities) run the repo's build scripts and proc macros. Rust ≥1.97.0 emits v0-mangled symbols (_R-prefixed names); check reversing tools support them.

Grep for Bug class Fix or confirm
AUD-1 unsafe impl Send/Sync Bounds looser than std's: data races Match std's bounds (UNS-7); Clippy non_send_fields_in_send_ty (opt-in)
AUD-2 set_len Uninitialized memory Miri; Clippy uninit_vec
AUD-3 as u8, as usize Silent truncation, NaN → 0 try_from (floats: (lo..=hi).contains(&x)); Clippy as_conversions (opt-in)
AUD-4 AssertSqlSafe, QueryBuilder, "SELECT…{x}" SQL injection: unchecked string, unsanitized push .bind(), .push_bind(); CodeQL rust/sql-injection
AUD-5 danger (reqwest, rustls verifiers) TLS verification off (reqwest, rustls): interception Delete
AUD-6 permissive, AllowOrigin::mirror_request CORS: any origin, with credentials List allowed origins
AUD-7 derive(Debug) on secrets Secrets in logs secrecy::SecretBox; CodeQL rust/cleartext-logging
AUD-8 == on MAC tags Timing leak CRY-5
AUD-9 build.rs, proc-macro Build-time code execution, dependencies' too Read first; VS Code Restricted Mode (no rust-analyzer) or a VM
AUD-10 .cargo/config*, rust-toolchain* Any program as rustc on most cargo commands, including rust-analyzer's; runner on run/test/bench Read first; run cargo audit --file <checkout>/Cargo.lock outside the checkout, so neither file loads
# grep and find only read files; nothing here runs the repo's code
REPO=path/to/untrusted/checkout
grep -rniE --include='*.rs' \
  -e 'unsafe impl.*(Send|Sync)' -e 'set_len' -e ' as [iu](8|16|32|64|128|size)' \
  -e 'AssertSqlSafe|QueryBuilder' -e '" *(select|insert|update|delete|where|order by) [^"]*[{]' \
  -e 'danger|permissive' -e 'AllowOrigin::mirror_request|allow_any_origin' \
  -e 'derive\(.*debug|^ *debug,' -e '(mac|tag)[^;]*==|==[^;]*(mac|tag)' "$REPO"
grep -rnE --include=Cargo.toml '^ *(build|proc[-_]macro) *=' "$REPO"
find "$REPO" -name build.rs -o -name 'rust-toolchain*' -o -path '*/.cargo/config*' -o -path '*/.vscode/*'

Go deeper

Crypto, TLS, secrets

Never hand-roll TLS, SSH or Noise: use rustls, russh ≥0.64.1 or snow ≥0.9.5.

Job Use (minimum version) Avoid (why) Status 2026-10-05
CRY-1 TLS rustls ≥0.23.45, default aws-lc-rs (X25519MLKEM768 first); rustls-platform-verifier (no revocation on Linux) webpki (idle since 2024-02; use rustls-webpki ≥0.103.13); *danger_accept_invalid*; ring with aws_lc_rs, or neither, without install_default() (builder() panics) 0.23.45, 2026-09-14
CRY-2 JWT jsonwebtoken ≥10.3.0, features = ["aws_lc_rs"], ≥256-bit key, require iss, aud; asymmetric signing if others verify (RFC 9068) No backend feature (panics); rust_crypto (pulls rsa) 11.1.0, 2026-09-16
CRY-3 Passwords argon2 ≥0.6.0 Argon2::default() (Argon2id at OWASP's minimum) Fast hashes like SHA-256 0.6.0, 2026-08-27
CRY-4 Encryption aes-gcm ≥0.10.3 or chacha20poly1305; random 96-bit nonces ≤2^32 messages per key, else counter or XChaCha20Poly1305 Reused nonce (catastrophic); sodiumoxide (deprecated) NCC-audited v0.3.0
CRY-5 MAC check hmac verify_slice or subtle ct_eq == on tags (timing leak) hmac 0.13.0, 2026-03-29
CRY-6 Tokens, keys getrandom::fill (Wasm: DOM-4) rng % len (biased); OsRng, RngCore (renamed in rand 0.10) 0.4.3, 2026-06-17
CRY-7 Signatures ed25519-dalek ≥2.0, curve25519-dalek ≥4.1.3; k256 ≥0.13.2; RSA: aws-lc-rs (constant-time) rsa signing, decryption (Marvin timing attack, unpatched) k256 NCC-reviewed 2023-08 (pre-0.14); p256, ecdsa unaudited
CRY-8 Post-quantum CRY-1 for TLS; aws-lc-rs (aws-lc-sys ≥0.39.0), libcrux-ml-kem ≥0.0.10 (RUSTSEC-2026-0212) or ml-kem pqcrypto, PQClean (archived); pqc_kyber (unpatched) ml-kem, ml-dsa unaudited; libcrux pre-0.1, partly verified
CRY-9 Secrets secrecy 0.10.3 SecretString from a secret store (wiped on drop, redacted Debug); exit if missing or empty Debug redaction alone (no wipe); dotenv (dotenvy, dev-only); env vars (leak to children, /proc, core dumps) Best-effort wipe: moved or reallocated copies remain
CRY-10 Hashing, key derivation sha2, blake3; hkdf MD5, SHA-1 (collisions) sha2 0.11.0, 2026-03-25
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
use serde::{Deserialize, Serialize};
const ISS: &str = "https://auth.example.com";
const AUD: &str = "orders-api";

#[derive(Serialize, Deserialize)]
struct Claims { sub: String, iss: String, aud: String, exp: u64 }

fn validation() -> Validation { // CRY-2: require every claim you check
    let mut v = Validation::new(Algorithm::HS256); // pinned; HS256 only if this service also signs
    v.set_issuer(&[ISS]);
    v.set_audience(&[AUD]);
    v.set_required_spec_claims(&["exp", "iss", "aud"]); // else tokens without iss/aud pass
    v
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut key = [0u8; 32]; // 256-bit demo key; in production, load it from a secret store
    getrandom::fill(&mut key)?;
    let exp = jsonwebtoken::get_current_timestamp().saturating_add(300);
    let claims = Claims { sub: "user-1".into(), iss: ISS.into(), aud: AUD.into(), exp };
    let token = encode(&Header::new(Algorithm::HS256), &claims, &EncodingKey::from_secret(&key))?;
    decode::<Claims>(&token, &DecodingKey::from_secret(&key), &validation())?; // never log `token`
    Ok(())
}

Go deeper

Hardening a service

Bound every input's time, size, nesting, file access.

Need Use (minimum version) Avoid (why) Status 2026-10-05
SVC-1 Slow clients (slowloris) HTTP/1 header_read_timeout, timer (below; axum); HTTP/2: cap max_concurrent_streams, proxy-enforced timeouts; TimeoutLayer::with_status_code; Semaphore-capped connections axum::serve, bare HTTP/2 (no header timeout) hyper 1.11.1
SVC-2 HTTP/2 floods h2 ≥0.4.16 0.3.x (unpatched); 0.4.0–0.4.3 (CONTINUATION flood) 0.4.19
SVC-3 Cross-site forgery (CSRF) tower-http CsrfLayer; GET changes no state very_permissive() CORS (AUD-6) 0.7.1
SVC-4 Body, WebSocket size RequestBodyLimitLayer; max_message_size <64 MB DefaultBodyLimit alone (skips streams), disable() axum 0.8.9
SVC-5 Length fields, decompression Cap before allocating; take(cap+1) on decoders, rejecting over cap bytes with_capacity(wire_len), unbounded decoders (OOM) std
SVC-6 Deserializing serde(try_from) validation; depth limits (serde_json: 128) derive(Deserialize) alone (skips validation); disable_recursion_limit, recursive postcard types (stack overflow) serde_json 1.0.151
SVC-7 Formats JSON, TOML; postcard for binary serde_yaml (archived); serde_yml (unsound); bincode (unmaintained, unlimited) 1.1.3
SVC-8 SQL sqlx ≥0.8.1 query!, push_bind push, AssertSqlSafe on input (injection; AUD-4) 0.9.0
SVC-9 User-named paths cap-std ≥4.0.3 (3.x: ≥3.4.6) Dir (CON-6) base.join(input) (.. or absolute input escapes) 4.0.3
SVC-10 Archives (tar/zip-slip) Into private empty directories: tar ≥0.4.46 unpack_in; zip ≥2.3.0 extract Entry::unpack (escapes target); enclosed_name() alone (symlinks escape) tar 0.4.46, zip 8.6.0
SVC-11 Child processes Rust ≥1.81.0 (.bat files); allowlisted args; env_clear() Untrusted args to cmd.exe, sh -c, raw_arg; inherited env (leaks secrets) std
SVC-12 User URLs (SSRF) timeout; Policy::none(); vet IP literals, dns_resolver results; no_proxy(); capped chunk() Defaults: no timeout, 10 redirects, system proxy reqwest 0.13.5
SVC-13 Logs (injection, leaks) tracing-subscriber ≥0.3.20 (escapes ANSI, not newlines); input as fields; instrument(skip_all) % (unescaped), messages with input (forged lines); bare #[instrument] (logs arguments) 0.3.23
SVC-14 Error responses Generic message; log source() thiserror {0}, transparent (leak internals) 2.0.21
use std::{convert::Infallible, io, sync::Arc, time::Duration};
use hyper::{Request, Response, body::Incoming, server::conn::http1, service::service_fn};
use hyper_util::rt::{TokioIo, TokioTimer};
// axum: copy only the Router-to-service_fn wiring from serve-with-hyper; keep this loop,
// since that example unwraps accept and binds 0.0.0.0. WebSockets need .with_upgrades().
async fn handle(_req: Request<Incoming>) -> Result<Response<String>, Infallible> {
    Ok(Response::new("ok".into()))
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> io::Result<()> {
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
    let slots = Arc::new(tokio::sync::Semaphore::new(1024)); // SVC-1: cap below the fd limit
    loop {
        let permit = slots.clone().acquire_owned().await.map_err(io::Error::other)?;
        let Ok((tcp, _)) = listener.accept().await else { // never unwrap (LIM-2): fds run out
            tokio::time::sleep(Duration::from_millis(100)).await; // back off, don't spin
            continue;
        };
        let conn = http1::Builder::new() // HTTP/1 only: HTTP/2 has no header timeout (SVC-1)
            .timer(TokioTimer::new()) // required: a timeout without a timer panics
            .header_read_timeout(Duration::from_secs(10)) // SVC-1: drop slowloris clients
            .serve_connection(TokioIo::new(tcp), service_fn(handle));
        tokio::spawn(async move { let _ = conn.await; drop(permit) }); // slot freed at close
    }
}

Go deeper

Which tool finds which bug

Match tool to bug class: Miri, sanitizers and fuzzers see only code that runs; Kani, only inputs its harness (test entry point) allows.

Bug class Catches it Misses it Cost
TOOL-1 Undefined behavior (UB) in unsafe Miri: cargo +nightly miri test; AddressSanitizer (ASan) (-Zsanitizer=address) for FFI Untested paths; ASan: aliasing, some bugs in uninstrumented C Nightly
TOOL-2 Data races in unsafe; atomic-ordering bugs Miri; ThreadSanitizer (TSan): RUSTFLAGS=-Zsanitizer=thread RUSTDOCFLAGS=-Zsanitizer=thread cargo +nightly test -Zbuild-std --target <triple>; loom (systematic) Miri: unexplored interleavings; TSan: atomic::fence; loom: some relaxed orderings TSan: nightly; loom: its types
TOOL-3 Panics, wrong results on untrusted input cargo-fuzz plus an oracle (below) Code the harness skips; unchecked output Nightly; harness per parser
TOOL-4 Panic and overflow sites; unsafe without // SAFETY: Clippy unwrap_used, indexing_slicing, arithmetic_side_effects, undocumented_unsafe_blocks Reachability; false SAFETY claims Stable; opt-in
TOOL-5 Injection (SQL, path, command, log) CodeQL, GA 2025-10-14 Classes outside its queries Private repos need Code Security
TOOL-6 Panic, overflow, out-of-bounds access, failed assert! (all inputs within bounds) Kani 0.68.0 Aliasing, invalid values, data races; inputs past harness bounds Harness per property
TOOL-7 Spec violations Verus, Creusot, Aeneas, hax, Flux, all active 2026-10 Wrong specs Specs and proofs
Vulnerable, malicious dependencies cargo-deny (DEP-1), cargo-vet (DEP-5) Unreported or non-RustSec advisories; audit gaps Stable; audits

Avoid Prusti (no default-branch commit since 2024-03-26; pins nightly-2023-09-15), Rudra and MIRAI (archived).

Fuzz your parser against an oracle (a correctness check):

// Setup: cargo install --locked cargo-fuzz@0.13.2 && cargo fuzz init
// In fuzz/fuzz_targets/fuzz_target_1.rs, make the target call the oracle:
//     fuzz_target!(|data: &[u8]| your_crate::check(data));
// Run: cargo +nightly fuzz run fuzz_target_1
use arbitrary::{Arbitrary, Unstructured};
#[derive(Arbitrary, Debug, PartialEq)]
pub struct Header { kind: u8, len: u16 }

pub fn encode(h: &Header) -> Vec<u8> {
    [&[h.kind][..], &h.len.to_be_bytes()].concat()
}

pub fn decode(bytes: &[u8]) -> Option<Header> {
    let &[kind, hi, lo] = bytes.first_chunk::<3>()?;
    Some(Header { kind, len: u16::from_be_bytes([hi, lo]) })
}

pub fn check(data: &[u8]) {
    let mut u = Unstructured::new(data);
    let Ok(header) = Header::arbitrary(&mut u) else { return };
    let _ = decode(u.take_rest()); // arbitrary bytes: may fail, must not panic
    assert_eq!(decode(&encode(&header)), Some(header)); // round-trip oracle
}

fn main() { check(&[1, 2, 3, 4]) } // smoke test; the fuzzer explores the rest

Go deeper

Binary hardening and sandboxing

Linux x86-64 builds already get address randomization, non-executable data, read-only relocations and stack probes (rustc); stack protectors and control-flow integrity need nightly, except Windows' -C control-flow-guard. Set the release profile and confine the process: Firecracker pairs Rust with KVM, seccomp syscall filters and a jailer.

Need Use (minimum safe version) Avoid (why) Failure prevented Status 2026-10-05
HARD-1 Overflow checks overflow-checks = true; cargo test --release ring <0.17.12: panics Silent wraparound Default off
HARD-2 Panic policy Servers: unwind, caught per request; stack overflow, OOM still abort (SVC-6) abort there: kills every request Whole-service outage Default unwind
HARD-3 Hide build paths --remap-path-prefix (RUSTFLAGS overrides config) Relying on strip: panic messages keep paths Leaked build paths --remap-path-scope stable 1.95; trim-paths unstable
HARD-4 Least privilege landlock 0.4.7 (Linux ≥5.13), default-deny seccompiler 0.5.0, before any thread or async runtime starts Ignoring RulesetStatus: may enforce nothing Post-exploit access seccompiler moved to rust-vmm
HARD-5 Untrusted Wasm wasmtime crates ≥49.0.2; fuel or epochs (CPU), StoreLimits (memory) Older: fuel bypassable; CVE-2026-34971 (aarch64), CVE-2026-34987 (Winch) escapes Sandbox escape, resource exhaustion Older lines: 48.0.4, 36.0.17

Go deeper

Domains: embedded, kernel, Wasm, safety-critical

Plan for hazards Rust doesn't check: overflowing firmware stacks overwrite statics, Wasm linear memory lacks internal guard pages and ASLR, and set_inner_html allows XSS. Tock isolates apps with hardware, yet its kernel had six isolation bugs Rust didn't catch. Run untrusted Wasm per HARD-5.

Need Use (minimum safe version) Avoid (why) Failure prevented Status 2026-10-05
DOM-1 Cortex-M stack flip-link, or cortex-m-rt ≥0.7.6 set-msplim (Armv8-M Mainline) Default layout (stack meets statics) Silent corruption flip-link 0.1.12, 2025-11-10
DOM-2 Linux kernel Recheck // SAFETY: claims wherever locks drop GCC-based builds (experimental) Binder-style races Non-experimental since 7.0
DOM-3 Wasm DOM output set_text_content; ammonia ≥4.2.1 (4.1.x: ≥4.1.5) for user HTML set_inner_html on input XSS 4.2.1, 2026-10-03
DOM-4 wasm32-unknown-unknown randomness getrandom wasm_js in binaries wasm_js in libraries (breaks non-Web builds) Broken builds 0.4.3, 2026-06-17
DOM-5 Wasm allocator Default wee_alloc (unmaintained) Leaks Last release 2019-08-22
DOM-6 Certified products Ferrocene: "the first quality-managed, TÜV SÜD–qualified, open source Rust toolchain" (ASIL D, SIL 3, Class C); Safety-Critical Rust Consortium and MISRA Rust addendum rules MISRA's safe-Rust ratings on unsafe code Unchecked unsafe/FFI FLS (language spec) adopted 2025-03-26; Consortium draft 0.1.25
// VULNERABLE (DOM-3): the browser parses `comment` as HTML, so `<img src=x onerror=…>` runs script
fn show(el: &web_sys::Element, comment: &str) {
    el.set_inner_html(comment);
}

fn main() {
    let doc = web_sys::window().and_then(|w| w.document());
    if let Some(el) = doc.and_then(|d| d.get_element_by_id("comment")) {
        show(&el, "<img src=x onerror=alert(1)>");
    }
}
// FIXED (DOM-3): the browser shows `comment` as plain text and never parses it as HTML
fn show(el: &web_sys::Element, comment: &str) {
    el.set_text_content(Some(comment));
}

fn main() {
    let doc = web_sys::window().and_then(|w| w.document());
    if let Some(el) = doc.and_then(|d| d.get_element_by_id("comment")) {
        show(&el, "<img src=x onerror=alert(1)>");
    }
}

Go deeper

Privacy tech

ZK proofs, threshold signatures and homomorphic encryption mostly fail in maths Rust can't check. Paillier is encryption, not multi-party computation: protocols using it must prove keys well formed.

Need Use (minimum safe version) Avoid (why) Prevents Status
PRIV-1 ZK circuit halo2_proofs; MockProver rejecting tampered private cells Under-constrained circuits (commonest); arkworks (prototype) Forgery 0.4.0, 2026-09-29
PRIV-2 Groth16 setup No setup: halo2; else per-circuit multi-party ceremony parameters generate_random_parameters (caller keeps "toxic waste") Forgery groth16 0.2.0, 2026-09-26
PRIV-3 Fiat-Shamir hash Every public input and commitment Partial transcripts (Frozen Heart) Forgery 2022-04-13
PRIV-4 zkVM (proven execution) risc0-zkvm ≥3.0.3 (2.x: ≥2.3.2) Old guest image IDs (CVE-2025-61588); 2.0.0–2.0.2 (CVE-2025-52484) Forgery 3.0.6, 2026-07-17
PRIV-5 Threshold ECDSA (t-of-n signing) cggmp24 = "0.7.0-alpha.3" Crate cggmp21 (unpatched); GG18/GG20 code like ZenGo multi-party-ecdsa (BitForge, CVE-2023-33241; unmaintained, RustSec silent) Key theft Pre-release, 2025-12-04
PRIV-6 Threshold Schnorr ZcashFoundation/frost, frost-<curve> ≥2.2.0 (RFC 9591) frost-secp256k1-tr, rerandomized FROST (unaudited) Weakened shares 3.0.0, 2026-04-23
PRIV-7 FHE tfhe-rs ≥1.1.0 defaults (IND-CPA^D: share decryptions of honest results only) IND-CPA parameters; Concrete (Python-first) Key recovery 1.8.1, 2026-09-14; no side-channel mitigation: isolate client-key hosts; commercial use: patent licence

Go deeper

Casebook

Rust code has had memory corruption (unsafe, std, compiler) and logic, panic, timing, TOCTOU, supply-chain failures. Newest first.

Disclosed Event Root cause Control Link
2026-09-21 Cached Miri output exposed CI secrets Env saved in target/ SUP-6 blog
2026-09-17 Video-call lures target crate owners Social engineering SUP-1 blog
2026-08-31 sudo-rs sudoedit privilege escalation Path resolved twice CON-6 GHSA-f42v-x7gq-phc8
2026-08-20 arrayref hijacked; proc-macro1 build.rs fetched malware Social engineering SUP-1, SUP-4, SUP-5 RUSTSEC-2026-0260, blog, 2026-09-17
2026-05-25 Third-party registries: credential leak, cache overwrite URL normalization; symlinks SUP-3 Rust ≥1.96.0 CVE-2026-5222, CVE-2026-5223
2026-04-22 uutils: 113 audit findings; Ubuntu kept GNU cp/mv/rm Open TOCTOU bugs CON-6 Canonical
2026-03-19 Cargo's tar let crates chmod directories Symlinks followed SUP-3 Rust ≥1.94.1; SVC-10 CVE-2026-33056, RUSTSEC-2026-0067
2026-03-10 chrono_anchor tried to exfiltrate .env files Malicious crate CRY-9 RUSTSEC-2026-0039
2025-12-16 Linux rust_binder corrupted kernel memory Racy SAFETY claim CON-7 CVE-2025-68260
2025-11-18 Cloudflare 5xx outage unwrap() on oversized file LIM-2 Cloudflare
2025-11-11 sudo-rs skipped authentication; replayed typed passwords Logic bugs LIM-4 CVE-2025-64517, CVE-2025-64170
2025-10 TickTock: six Tock isolation bugs; an underflow crashed the kernel Logic errors, missed checks TOOL-7, LIM-3 TickTock
2025-09-12 rustfoundation.dev phishing for GitHub logins Look-alike domain SUP-1 blog
2024-06-18 curve25519-dalek leaked timing LLVM inserted a branch CRY-7 ≥4.1.3 RUSTSEC-2024-0344
2024-04-09 Windows batch-file injection in Command Incomplete escaping SVC-11 Rust ≥1.81.0 CVE-2024-24576, CVE-2024-43402
2024-02-16 cve-rs: memory corruption without unsafe Open compiler bug #25860 LIM-1 RUSTSEC-2025-0028, releases
2022-01-20 std remove_dir_all symlink race TOCTOU CASE-1 Rust ≥1.58.1 CVE-2022-21658
2018–2021 std buffer overflows, double frees Bugs in std's unsafe CASE-1 Rust ≥1.52.0 CVE-2018-1000810, CVE-2021-31162
  • CASE-1 Update Rust promptly, then rebuild and redeploy: binaries embed std.

Go deeper

  • RustSec: use for crate, malware advisories.

Stay current

Found advice here that could make code less safe? Report it through private vulnerability reporting. Other errors: open an issue or PR.

About

Where Rust's safety guarantees stop, and how to secure Rust code, dependencies, builds and services. Every claim sourced, every example compiled in CI.

Topics

Resources

Stars

20 stars

Watchers

1 watching

Forks

Sponsor this project

Used by

Contributors

Languages