Frontend Labs
⋯

Part VI — Security-focused Rust

Rust removes the bug classes behind most of the C guide's Part V. It does not remove the rest of the OWASP Top 10. This Part is precise about where the line falls, then covers the security work that is still yours: cryptography, input handling, testing hostile input, and dependencies.


51. What Rust prevents, and what it does not

Bug class In safe Rust Notes
Buffer overflow / out-of-bounds read prevented bounds checks; a bad index panics
Use-after-free, double free, dangling pointers prevented ownership and borrowing
Data races prevented Send/Sync
Uninitialized memory reads prevented definite-assignment checking
Null-pointer dereference prevented Option
Type confusion prevented no unchecked casts outside unsafe
Integer overflow defined, not prevented panics in debug, wraps in release by default (§3.3)
Panics (unwrap, indexing, overflow) possible a remotely triggerable panic is a DoS bug
Deadlocks, logical race conditions possible memory-safe, therefore allowed (§29.2)
Memory leaks possible Rc cycles, mem::forget, unbounded caches
Injection (SQL, shell, template, log) possible use parameterized APIs; types can help (§53)
Broken access control, auth logic possible the most common serious web bug, in any language
Timing side channels possible == on secrets leaks timing (§52.3)
Secrets in logs possible #[derive(Debug)] prints every field (below)
Memory-safety bugs in unsafe or C dependencies possible audit unsafe, run Miri (§33.4)
Malicious or vulnerable dependencies possible Ch 55

The real-world numbers. Google reported that as Android shifted new native code to Rust and other memory-safe languages, memory-safety bugs fell from 76% of Android's vulnerabilities in 2019 to 24% in 2024. Google had reported in 2022 that, by then, no memory-safety vulnerabilities had been found in Android's Rust code. The bugs that remain in Rust codebases are the right-hand column of this table: logic, authorization, injection, and DoS through panics.

51.1 Secrets leaking through Debug

use std::fmt;

#[derive(Debug)]
struct LoginBad { user: String, password: String }

/// A wrapper whose Debug and Display never reveal the value.
#[derive(Clone)]
struct Secret<T>(T);
impl<T> Secret<T> { fn expose(&self) -> &T { &self.0 } }   // the only way in: greppable in review
impl<T> fmt::Debug for Secret<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("Secret([REDACTED])") }
}

#[derive(Debug)]
struct LoginGood { user: String, password: Secret<String> }

fn main() {
    let bad = LoginBad { user: "ada".into(), password: "hunter2".into() };
    let good = LoginGood { user: "ada".into(), password: Secret("hunter2".into()) };
    println!("{bad:?}");   // LoginBad { user: "ada", password: "hunter2" }  <- ends up in your logs
    println!("{good:?}");  // LoginGood { user: "ada", password: Secret([REDACTED]) }
    println!("{}", good.password.expose().len());
}

tracing::info!(?request, "login") debug-prints the whole struct, so one #[derive(Debug)] on a request type can send passwords to your log pipeline. The secrecy crate provides a production version of Secret<T> (SecretString) that also zeroizes the value on drop. Security from Zero Ch 49 covers the operational side.

51.2 Panics are a denial-of-service surface

let n: usize = header.parse().unwrap(), &body[..4], a / b with b == 0, and x + y overflowing in debug builds are all panics an attacker can trigger. Rules for code that handles untrusted input:

  • Use get, checked_*, ?, and split_at_checked instead of indexing and unwrap.
  • Turn on clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, and clippy::arithmetic_side_effects for parser modules (#![deny(...)] at the top of the module).
  • Fuzz the parser (Ch 54). Fuzzers find panics within minutes.

Generated from real-life-rust-guide.md · 8 Oct 2026 16:36