The Rust Engineering Handbook / Chapter 91
Security Engineering: Memory Safety Is Not a Threat Model
Model the assets, authorities, trust crossings, abuse cases, and residual vulnerabilities that remain in memory-safe Rust systems.
An attacker sends a validly encoded admin request to relay-service:
POST /admin/queues/purge
identity: operator@tenant-a
body: { "tenant": "tenant-b", "queue": "priority" }
The parser stays within bounds. Every String is valid UTF-8. No pointer is dangling, no data race occurs, and no unsafe block executes. If the handler checks only that the caller has an operator role, it can still erase another tenant’s queue.
Memory safety removes important exploit primitives. It does not establish who the caller is, which object that identity may control, whether input becomes code, whether a path remains below a root, whether work is economically bounded, whether a secret reaches telemetry, or whether cryptography is used in a sound protocol. The security contract is broader: for every effect, authenticate the principal, authorize the exact operation and resource, validate data at its interpretation boundary, bound attacker-controlled cost, protect sensitive assets across their lifetime, and contain the result if one control fails.
Start the review with assets and authorities
A threat model is not a generic list of bad things. Name what the system protects and what power each component holds. For the relay, assets include tenant messages, queue ordering, delivery credentials, audit history, availability budgets, deployment signing keys, and the integrity of operator controls. Availability is an asset: an attacker who cannot read a record may still make the service too expensive to operate.
Then list principals and authorities. A public client may submit within its tenant and quota. A worker may dequeue but not change tenant policy. An operator may alter bounded controls for assigned tenants. A release job may sign an artifact but should not read production message bodies. “Internal” is not a principal, and network position is weak evidence of intent.
Authentication answers which principal presented acceptable evidence. Authorization answers whether that principal may perform this operation on this resource under current policy. Keep the questions separate. A valid token can be stolen, over-scoped, issued for another audience, or correctly identify a caller that lacks object-level permission.
The fixture makes the resource binding visible:
pub fn authorize_purge(
role: Role,
authenticated_tenant: &str,
command: &AdminCommand<'_>,
) -> bool {
role == Role::Operator
&& authenticated_tenant == command.tenant
&& valid_identifier(command.queue)
}
Real policy will be richer, but the review question is durable: which authenticated fact binds each attacker-controlled resource identifier? A role-only check, a hidden UI button, or a UUID that is difficult to guess is not object authorization. Central policy improves consistency; local enforcement near the effect preserves context. A practical design uses both: a typed identity and policy decision enter the application layer, and the resource adapter refuses operations that lack the required scoped capability.
Use the threat map as an adversarial trace rather than an architecture poster. Follow each colored flow across a dashed boundary, then ask where identity evidence, parser output, queued work, or administrative authority changes meaning without a corresponding control.
Draw boundaries where evidence and authority change
The diagram is useful only if its arrows correspond to actual deployed flows. Mark process, host, account, network, tenant, and administrative boundaries. A TLS terminator, message broker, sidecar, object store, CI runner, browser, and operator laptop may each create a distinct trust crossing even when they share an organizational owner.
For each flow, record format, maximum size, identity evidence, confidentiality need, integrity protection, replay semantics, timeout, and failure destination. Then ask four repeatable questions: What are we building? What can an adversary make it do? What prevents or detects that? How will we verify and revisit the answer? A data-flow diagram without abuse cases is architecture documentation; an abuse list without a concrete flow is difficult to test.
Trust is scoped, not binary. The queue may be trusted to preserve authenticated bytes but not to decide tenant policy. A parser may be trusted to produce a syntax tree but not to prove a command is authorized. A sandboxed helper may process hostile media while remaining untrusted with secrets. Express narrow authority through separate credentials, handles, processes, accounts, network policy, filesystem roots, and typed capabilities rather than one service identity with ambient access.
Treat every interpreter as an injection boundary
SQL injection is one instance of a wider pattern: attacker-controlled bytes cross into a language with effects. Shells, query languages, templates, regular expressions, paths, URLs, log formats, archive names, configuration fragments, and even metrics labels are interpreters.
Prefer structured APIs that separate data from grammar. Pass command arguments directly instead of constructing a shell string. Bind database values instead of quoting them manually. Escape for the exact output context at the final boundary; HTML escaping does not make input safe for JavaScript or a URL. Validate identifiers against a deliberately narrow grammar when an API cannot parameterize them.
Paths need both lexical and operating-system reasoning. The fixture rejects absolute names and .. components:
pub fn safe_object_path(root: &Path, untrusted: &str) -> Option<PathBuf> {
// Accept only normal relative components beneath a trusted root.
}
That prevents obvious traversal but does not defeat a symlink swap between validation and open. Canonicalizing then opening by a string can still create a time-of-check/time-of-use race. Stronger designs use platform facilities that resolve relative to an already opened directory, constrain symlink traversal, run under a filesystem view containing only required objects, or move untrusted content processing into a sandbox. Be explicit about portability: the exact race-resistant primitive and flags differ by operating system.
Command handling should normally avoid a shell entirely. If a shell is the product requirement, treat the command text as code: restrict who can supply it, isolate the execution identity, constrain environment and filesystem, set resource limits, capture an audit record, and accept that escaping is not a universal proof.
Bound parsers by work, not only bytes
A 4 KiB document can trigger extreme recursion, backtracking, allocation, hash collisions, decompression expansion, or a graph with pathological relationships. Chapter 90 bounded accidental demand; security review asks how an adversary chooses the worst accepted input repeatedly.
Put limits at each representation: compressed bytes, expanded bytes, nesting depth, token count, string length, collection members, distinct labels, reference edges, parse time, and downstream fan-out. Use checked integer arithmetic for lengths, offsets, products, and unit conversions. Rust’s debug and release overflow behavior is not a business policy; choose rejection, saturation, wider arithmetic, or a proven bound deliberately.
Streaming avoids retaining the entire input but does not automatically bound CPU or emitted work. Early validation may itself be expensive. Measure adversarial families, not just representative fixtures: deeply nested structures, long common prefixes, duplicated keys, invalid terminators, extreme numeric forms, and inputs that fail at the last byte. Put a total budget around the request so several individually bounded stages cannot compose into unbounded cost.
Parser differentials matter when multiple components interpret the same bytes. If the edge normalizes a path or header differently from the service, an attacker can obtain one authorization decision and another effect. Define one canonical representation and sign or authorize that representation, or reject ambiguous encodings before trust is transferred.
Logic, races, unsafe code, and FFI remain security surfaces
Rust prevents data races in safe code, not all race conditions. Two valid operations can interleave incorrectly: check quota then reserve; verify ownership then delete; revoke a token while an accepted job retains authority; replace a path after validation; or issue two idempotency claims before either commits. Protect the business transition with an atomic database condition, unique constraint, transaction, lease, compare-and-swap, or protocol state machine. A mutex around one process is insufficient when the state spans replicas or external systems.
unsafe and FFI deserve an obligation ledger from Part XI. For security, extend it with attacker reachability, input size, process authority, sandbox boundary, and exploit consequence. A memory error in a media decoder running without secrets in a disposable worker is not equivalent to the same decoder linked into the control plane. Validate foreign lengths and ownership before constructing Rust references or slices; constrain callbacks, unwinding, thread affinity, and lifetime on both sides. Review compiler flags and native transitive libraries because Rust’s guarantees do not retroactively govern foreign code.
Sandboxing is containment, not validation. Use a separate process, minimal OS identity, read-only filesystem, restricted network, bounded CPU/memory/process count, and a narrow message protocol when the damage reduction justifies operational cost. Containers alone do not define a security boundary; the kernel, runtime configuration, mounted credentials, capabilities, and escape response are part of the argument.
Keep secrets out of ordinary data paths
Secrets leak through debug formatting, errors, traces, panic reports, command lines, environment inspection, heap dumps, crash cores, metrics labels, test fixtures, and copied support bundles. A type whose Debug implementation redacts is useful but cannot stop explicit access or copies made by dependencies. Minimize acquisition, scope credentials to one purpose and audience, keep them out of long-lived application state, and ensure telemetry accepts allow-listed fields rather than arbitrary objects.
Zeroizing memory can narrow residual exposure but is not a complete guarantee: copies, optimizer behavior, swap, core dumps, and downstream libraries matter. Rotation needs dual-key transition, cache invalidation, failure visibility, and an emergency revocation path. Never log a secret to prove redaction works; test the serialization and telemetry policies with canary values.
Side channels concern information revealed by timing, cache behavior, response shape, resource contention, or error detail. Constant-time comparison is necessary for some cryptographic values but does not make an entire protocol constant time. Rate limits, uniform external errors, isolation of sensitive workloads, and reviewed cryptographic libraries can reduce exposure. Do not design a new cipher, mode, signature encoding, nonce scheme, or key derivation protocol because the underlying primitive is available. Choose an established high-level construction matched to the threat model and arrange independent cryptographic review.
Secure defaults matter because optional controls disappear under incident pressure. Bind admin listeners narrowly, deny unknown roles and fields, disable diagnostic endpoints, require TLS verification, cap sizes and work, and make insecure development modes fail to start in production. A configuration warning is not enforcement.
Perform the abuse-case review
Review the relay parser and admin API as an adversary. For each case, name the asset and entry flow, then describe the attacker’s precondition, abuse action, and security effect. Carry the review through prevention, detection, recovery, ownership, and verification. At minimum, test these cases:
- A valid operator identity supplies another tenant’s identifier.
- A queue name contains separators, alternative encodings, control characters, or a last-moment symlink swap.
- A small compressed frame expands beyond memory and CPU budgets.
- A syntactically valid parser input causes worst-case nesting, fan-out, or late failure.
- Two purge or quota transitions race across replicas.
- An error includes request content, bearer credentials, or cryptographic material.
- A compromised native parser or build artifact attempts network and credential access.
- A degraded mode or operator override bypasses authentication, authorization, durability, or audit invariants.
For every mitigation, name a verification method: unit or property test, rejected request corpus, concurrency model, fault injection, log-leak scan, permission test, sandbox escape test, or incident rehearsal. Detection without a bounded response is incomplete. Prevention without telemetry can silently rot.
Security review questions
- Are assets, principals, authorities, flows, and trust boundaries explicit in the deployed architecture?
- Does every effect bind authenticated identity to operation and exact resource scope?
- Are interpreters handled by structured APIs or context-specific validation at the final boundary?
- Are paths protected against traversal and the relevant symlink/TOCTOU races?
- Are bytes, expansion, nesting, CPU, allocations, fan-out, retries, and labels bounded?
- Are integer conversions and arithmetic governed by explicit rejection or saturation policy?
- Can logic races violate authorization, quota, deletion, or idempotency across replicas?
- Are unsafe, FFI, native libraries, and parsers isolated according to attacker reach and consequence?
- Can secrets enter formatting, telemetry, crash, support, test, or command-line paths?
- Are cryptographic constructions high-level, established, version-pinned, and independently reviewed?
- Do secure defaults fail closed, and do operator controls preserve security invariants?
- Does each control have prevention, detection, recovery, ownership, and repeatable evidence?
Memory safety changes the threat landscape in a valuable way: many corruptions become compiler errors or contained panics rather than attacker-controlled writes. It does not change the need to model intent, authority, cost, information flow, and containment. Apply that reasoning before third-party code reaches the build as well: source selection can execute code long before the service handles a request.
Sources and version notes
- OWASP Threat Modeling Cheat Sheet describes a repeatable decomposition, threat identification, mitigation, and validation loop.
- OWASP Authorization Cheat Sheet, OS Command Injection Defense, and Path Traversal provide boundary-specific review guidance; adapt it to the deployed protocol and platform.
- Rust Reference: behavior considered undefined and the Rustonomicon FFI chapter define relevant language and foreign-boundary obligations, not a whole-system threat model.
- OWASP Cryptographic Storage Cheat Sheet emphasizes threat-model-driven data and key protection; use current, reviewed high-level libraries and organizational cryptographic standards.
The fixture targets Rust 2024, declares MSRV 1.85, and has no third-party dependencies. Its tests demonstrate narrow authorization and lexical path rules; they do not prove operating-system race resistance, parser complexity bounds, sandbox strength, cryptographic correctness, or production policy.
Continue reading
Full table of contents