Skip to main content
Threat Modeling Protocols

What to Fix First When Your Threat Model Ignores Protocol State Machines

You've done the threat model. You've listed assets, drawn trust boundaries, and flagged spoofing risks. But your model doesn't consider what happens when messages arrive out of order, or when a node holds a stale session state. That's the gap protocol state machines fill—and if you ignore them, you're blind to half the attack surface. Here's the fix: start with the transitions. Not the states themselves, but which transitions are legal and which are missing. In TLS 1.3, for instance, a server that accepts a ClientHello after sending Finished is in an undefined state—attackers can exploit that. OAuth's authorization code flow has a hidden state (one-time code already exchanged) that, if not enforced, leads to code injection. In BGP, missing hold timer transitions let attackers keep stale routes alive. This article walks through what to fix first, in priority order. Where State Machine Blind Spots Hit Real Protocols TLS 1.

You've done the threat model. You've listed assets, drawn trust boundaries, and flagged spoofing risks. But your model doesn't consider what happens when messages arrive out of order, or when a node holds a stale session state. That's the gap protocol state machines fill—and if you ignore them, you're blind to half the attack surface.

Here's the fix: start with the transitions. Not the states themselves, but which transitions are legal and which are missing. In TLS 1.3, for instance, a server that accepts a ClientHello after sending Finished is in an undefined state—attackers can exploit that. OAuth's authorization code flow has a hidden state (one-time code already exchanged) that, if not enforced, leads to code injection. In BGP, missing hold timer transitions let attackers keep stale routes alive. This article walks through what to fix first, in priority order.

Where State Machine Blind Spots Hit Real Protocols

TLS 1.3 handshake and the Finished message race

Most teams skip this: the Finished message in TLS 1.3 isn't just a polite 'hello done' flag—it's a cryptographic commitment that locks the handshake state. I have watched an implementation where the server accepted application data before validating the client's Finished. That's a state machine gap, not a crypto bug. The spec says the client is in 'Wait for Finished' until that message checks out, but the code let the state flip to 'Connected' prematurely. Attackers didn't break AES—they just skipped a state transition. The result? A downgrade into a partially authenticated tunnel. That hurts.

The catch is that most TLS fuzzers hammer message formats, not state sequences. They'll mutate a Certificate payload until something crashes, but they won't send a Finished before the ClientHello. That's the blind spot. Your threat model might mention 'cipher suite negotiation' and 'certificate validation' but leave out the exact order of FLIGHT transitions. Quick reality check—a protocol that passes all format checks can still fail every state check. You fix the wrong thing first when you patch the parser but ignore the state machine.

OAuth 2.0 authorization code reuse vulnerabilities

OAuth 2.0's authorization code flow has a quiet killer: a code that fires twice. The spec says a code must be single-use, but what does your state machine say? I fixed one where the authorization server accepted a code, issued a token, then accepted the same code again five seconds later. The code's state never moved from 'Valid' to 'Consumed.' The threat model had rows for 'token leakage' and 'redirect URI validation'—zero rows for 'code state transitions.' So the seam blew out: an attacker who intercepted one callback could replay the code, get a second token, and maintain access after the legitimate user logged out.

The tricky bit is that OAuth libraries often expose the protocol as a series of HTTP endpoints, not as state transitions. Engineers read the RFC, see 'authorization code is single-use,' and assume the database check will catch duplicates. But without an explicit state that says 'this code is now CONSUMED and can't transition back to VALID,' you rely on application logic that bends under load. We found the bug because the database write for 'Consumed' was asynchronous—state was still 'Valid' in the read replica. Classic. Your protocol diagram shows arrows; your threat model needs to show which arrows are one-way only.

BGP route announcement and hold timer resets

BGP's hold timer is a textbook state machine weapon. The RFC says: if you receive a Keepalive or Update, reset the hold timer to zero and start counting again. Sounds boring. But BGP's finite state machine has an OPEN state, an ESTABLISHED state, and a very long list of transitions that do reset the timer versus those that don't. I saw a corner where a router accepted a malformed route withdrawal—technically an Update message—but the parser failed silently and the state machine stayed in ESTABLISHED. The hold timer didn't reset because the message was discarded before the state layer ever saw it. The peer kept waiting. That's how a single bad message times out an entire session across a backbone.

Most teams skip this: they model BGP's threat surface as 'prefix hijacking' and 'AS path manipulation' but never diagram the three-way handshake between message arrival, parser result, and timer reset. The state machine is a dependency graph—one missed edge and the whole session dissolves. Wrong order again: throwing more RPKI filters at route leaks won't help if your state machine lets a silent discard kill the hold timer. You fix the timer logic first, then the cryptographic attestations. That's the order nobody follows.

'A protocol that passes every format check can still fail every state check. State machines aren't optional detail—they're the seam where attacks land.'

— engineer who spent a weekend untangling a BGP hold-timer bug that turned out to be a state transition, not a routing policy issue

State Machine vs. Stateful Protocol: What Engineers Confuse

State machine = set of states + transitions; stateful = memory of past messages

Most teams treat these as synonyms. They're not. A state machine is a diagram—nodes and arrows that define every reachable condition of your protocol. Stateful design is a property: your system remembers what happened before. The confusion? Engineers build stateful systems without mapping the machine. That hurts. I've watched teams say "we have a state machine" when they've really just stashed a few bits in memory—message type A sets a flag, message type B clears it, and nobody drew the diagram. The protocol works in happy path testing, then blows up when a message arrives out of order that the code never considered a valid transition. The state machine would have caught that. The stateful flag-cluster? It just fails silently.

The real trap is thinking "we added state" means "we modeled state." Quick reality check—if you can't write down every valid transition in under thirty arrows, your threat model is flying blind. Attackers love this gap. They find the undefined edge—the sequence your code never anticipated—and the seam just blows out.

Why stateless protocols (HTTP GET) still have implicit state machines

Stateless sounds safe. No memory, no state machine—right? Wrong. HTTP GET appears stateless because each request carries its own context, but the protocol itself is a state machine. The client starts in IDLE, moves to SENT, awaits RESPONSE, loops back. That's a machine with three states and explicit transitions. The catch is that threat modelers often skip this because "it's just HTTP." But think about retry storms: a client that stays stuck in WAITING because the server never sends RST—that's an error state omission. The implicit machine matters because attacker tooling exploits transition gaps, not data content. I have seen a red team force a stateless protocol into a livelock by replaying a partial request sequence. No flags were changed. The state machine just didn't define what happens after a mid-stream disconnection. That's the gap.

“Every protocol, even the ones that claim statelessness, has an embedded state machine. Ignoring it means you're threat-modeling a skeleton without the joints.”

— paraphrase from a protocol auditor after finding his 14th missed transition in a "simple" query loop

The 'just track flags' fallacy and missing sink states

This is the most common mistake I see. A team says "we handle state—we have a boolean for handshake-complete, a boolean for authenticated, and a counter for retries." That's not an FSM. That's a variable soup. The problem isn't the flags themselves—it's that flags have no transition rules. Two booleans give four theoretical states. Most code only handles three, leaving one unreachable in testing but reachable in production under race conditions. That dangling corner? Attackers find it. Worse, flags don't model sink states—terminal conditions where the protocol should stop talking. A genuine state machine has explicit DEAD, ERROR, and TIMEOUT sinks. Flag-based logic almost never does. The protocol just sits there, waiting for a message that will never come, logging nothing, burning resources. That's a free denial-of-service vector.

I fixed one protocol where the handshake flags permitted an unauthenticated read after connection reset—the code never cleared a flag on teardown. The threat model that skipped sink states missed it entirely. The fix? Three states: CONNECTING, ACTIVE, TERMINATED. That one diagram closed the hole. The flag version had nine variables and still couldn't describe what "done" meant.

Start by asking: if two messages arrive in reverse order, does your code know that's invalid—or does it silently interpret them? If the answer requires tracing three if-statements, you're in flag territory. Draw the machine. You'll find the missing sink faster than you'd expect.

Patterns That Actually Work: Deterministic Transitions and Error States

Explicit dead-end states for unexpected messages

The cleanest fix I have seen teams apply is brutally simple: if a protocol message arrives in a state where that message makes no sense, you terminate the session—not silently, not with a warning that gets logged and ignored. You hard-stop. One WebSocket handshake implementation I audited accepted a PONG frame before the handshake completed. The developers thought this was harmless—extra credit, even. It turned into a resource leak that let one client pin a server thread indefinitely. The fix was a single transition row in the state table: Connecting + PONGClosed, send a protocol error code. That's it. No conditional logic, no forgiveness.

Teams often resist this—"But what if the client just sent it early by accident?"—which misses the point. Accident or malice, the state machine doesn't care. You're buying determinism at the cost of tolerance. That trade-off is worth it when the alternative is an attack surface shaped like a fishing net. Most bugs in stateful protocols come from states the designer never wrote down; an explicit dead-end catches them before they metastasize into CVEs.

A transition you didn't define is a vulnerability you haven't met yet. Define the crash state before the crash finds you.

— paraphrased from a production postmortem at a messaging infrastructure team

Idempotent transitions and replay protection

The catch with idempotency is that engineers confuse side-effect free with safe to repeat. They're not the same. Consider a payment authorization protocol: receiving a duplicate AUTHORIZE message in AwaitingSettlement should not double-charge the user. That's idempotent at the application layer. But what if that duplicate arrives in AwaitingAuth instead? You need the state machine to absorb the replay without drifting—same input, same output, same next state. We fixed this inside a token-exchange flow by storing a monotonic counter in the session and rejecting any message with a sequence number

Share this article:

Comments (0)

No comments yet. Be the first to comment!