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_*,?, andsplit_at_checkedinstead of indexing andunwrap. - Turn on
clippy::unwrap_used,clippy::expect_used,clippy::indexing_slicing, andclippy::arithmetic_side_effectsfor parser modules (#![deny(...)]at the top of the module). - Fuzz the parser (Ch 54). Fuzzers find panics within minutes.