CWE-691: Insufficient Control Flow Management
Overview
This weakness covers code that does not sufficiently manage its own control flow, letting execution reach a state the developer did not intend: a state transition is allowed without validating that the current state permits it, a handler that should run is missing or is the wrong one, two operations happen in the wrong order, or an exception unwinds past the frame that was supposed to contain it. Attackers exploit this by forcing the mismanaged path - sending unexpected input, racing a state change, triggering an error branch nobody exercised - to reach code that should have been unreachable.
Relationship to Other CWEs
CWE-691 is a MITRE Pillar - the most abstract level of the hierarchy, a theme covering the whole control-flow family rather than a single reportable weakness - and MITRE's mapping guidance for it is Discouraged: a real finding should carry a descendant's number instead.
- CWE-691 (this page) - control flow or state management that does not hold, with no more specific weakness identified
- CWE-362 (Race Condition) - a descendant with dedicated guidance here, under CWE-662, and the better page when the mismanaged path is one an attacker interleaves with
- CWE-248 (Uncaught Exception) - the other descendant with a page here, under CWE-705, for when control leaves the frame that was meant to contain the failure
- CWE-841 (Improper Enforcement of Behavioral Workflow) - no page here, and the number to file for the workflow-ordering pattern below
- CWE-431 (Missing Handler) - no page here, and the number for the dispatch pattern below that, which is the case with no handler at all
- CWE-430 (Deployment of Wrong Handler) - CWE-431's sibling, where a handler does run and is the wrong one. Discussed below but not demonstrated, because the two are found differently: a missing default branch is visible in the source, and a wrong handler is only visible in what is actually registered at runtime
- CWE-252 (Unchecked Return Value) - not a CWE-691 descendant at all, and the shape findings most often arrive here in. Its parent CWE-754 sits under a different pillar, CWE-703 (Improper Check or Handling of Exceptional Conditions). What Belongs on Another Page, at the end, says why it lands here; the remediation detail - privilege-drop verification, short reads, compiler enforcement - is on CWE-252
MITRE's full list of direct children, most with no page here: CWE-430 (Deployment of Wrong Handler), CWE-431 (Missing Handler), CWE-662 (Improper Synchronization), CWE-670 (Always-Incorrect Control Flow Implementation), CWE-696 (Incorrect Behavior Order), CWE-705 (Incorrect Control Flow Scoping), CWE-768 (Incorrect Short Circuit Evaluation), CWE-799 (Improper Control of Interaction Frequency), CWE-834 (Excessive Iteration), CWE-841 (Improper Enforcement of Behavioral Workflow) and CWE-1281.
Use this page when a finding is reported generically as a control-flow or state-management problem with no more specific CWE, or as background on what the family has in common.
OWASP Classification
A06:2025 - Insecure Design
Risk
Medium-High: Mismanaged control flow lets attackers reach privileged code through an invalid state transition (marking a session authenticated without going through the login path), act in the gap between a check and the operation it authorises, or land on a path where the handler that should have run is missing or is the wrong one. Impact depends on what the unreachable-by-design code did; authentication and authorization bypass are the common outcomes, because that is where "you cannot get here without doing X first" is load-bearing.
Remediation Steps
Core Principle: Make the states and orderings the design forbids unreachable in code rather than merely unlikely - a step that must not be skipped needs a precondition that stops it, not a convention that discourages it.
Trace the Data Path
- Source: The design's own assumption about order or state - "the user is authenticated by the time this runs", "the payment is captured before the item ships", "only the login path sets this flag".
- Sink: The code that assumption protects, and every route into it - not only the one the feature was written for. Alternate entry points are where this weakness lives: an admin tool, an OAuth callback, a retry job, a test helper.
- Missing control: No precondition at the sink re-establishing what the caller was supposed to have done, so reaching it by an unintended route succeeds.
Enforce Explicit State Machines (Primary Defense)
Where an object has meaningful states (unauthenticated -> authenticated -> expired), require every transition to validate that the current state legally allows it, and reject the transition otherwise instead of silently allowing it. Make the state private and the transition the only way to change it, so "what can authenticate this session" has one answer rather than one per assignment site.
// SECURE - pseudo-code
function authenticate(session, credentials):
if session.state != UNAUTHENTICATED:
raise IllegalStateError
if not verify(credentials):
raise AuthenticationError
session.state = AUTHENTICATED
Give Every Path a Handler, and Check It Is the Right One
A path with no handler falls back to whatever the framework does by default, which was not written for this decision. MITRE files the missing-handler and wrong-handler cases as CWE-431 and CWE-430.
- Make the default branch of a dispatch reject rather than fall through: an unrecognised state, role, message type or file extension is a case the design did not anticipate, and continuing is the wrong answer to it
- Check the handler that is actually registered, not the one the configuration appears to name - a handler registered for a subclass the framework never raises, or one shadowed by an earlier registration, is absent in effect
- Where the handler is chosen from a property of the object rather than from a fixed set of cases, check the type before interpreting it and reject inconsistent ones. A declared type that disagrees with the content is what routes an object to a handler written for something else, and no default branch catches it because the dispatch matched
- Where a framework supplies a default handler, decide explicitly whether to keep it; inheriting it is a decision either way
Keep a Check and the Operation It Authorises Together
Two operations in the wrong order is CWE-696, and the security-relevant version is a check separated from the act it permits.
- Perform the check and the operation against the same state. If the state can change in between, an attacker who can influence the timing gets the operation without the check - that is CWE-362, and it needs that page's synchronization guidance rather than a re-ordering
- Validate after transforming, not before: a check applied to a value that is later decoded, canonicalised or normalised was applied to a different value than the one that reaches the sink
- Watch for side effects in a short-circuiting expression (CWE-768) - an operand that performs the audit log, the counter increment or the revocation check is skipped whenever an earlier operand settles the answer
Use Guard Clauses and Fail-Closed Defaults
Reject invalid conditions early with explicit guard clauses rather than nesting the valid case inside conditionals that are easy to get wrong, and default to denying access rather than granting it when a check is inconclusive.
Test with Malicious Inputs
- Attempt state transitions out of order - authenticate twice, access data before authenticating, resume a completed workflow, replay a step - and confirm each is rejected rather than silently accepted.
- Reach the protected code by every route that exists, not the one the feature was written for, and confirm each route re-establishes the precondition.
- Send a value no branch handles - an unknown enum, role, or message type - and confirm the default branch rejects rather than falls through.
- Where a check and its operation are separate statements, change the state in between (another session, another request, a concurrent call) and confirm the operation fails.
- Re-scan or re-run static analysis to confirm unreachable-default-case and workflow-order findings are resolved.
Common Vulnerable Patterns
A state transition made without the check that authorises it
Why this is vulnerable: the flag is a record of a decision, and here it is being written by code that did not make the decision. Whatever the login path does - verify a password, check a second factor, consult a lockout counter - is bypassed entirely, not defeated, because this line reaches the same end state by a different route.
These accumulate where the session object is writable from anywhere in the application. An impersonation feature, a password-reset flow, a test helper, or an OAuth callback each need to establish a session, and each tends to do it by setting the field directly rather than by calling the one function that is allowed to. The number of paths that can authenticate then equals the number of places that assignment appears, which is not a number anyone is tracking. Making the transition available only through a single method that performs the checks turns a field that anything can set into an operation with a precondition.
A workflow step reached without the step before it
// VULNERABLE - pseudo-code
function ship_order(order_id):
order = load(order_id)
mark_shipped(order) // no check that payment was ever captured
Why this is vulnerable: the ordering exists in the design and nowhere in the code. Every legitimate caller arrives here from the payment step, so the sequence holds in testing and in production traffic, and holds only for as long as nobody calls ship_order directly. An attacker who can reach the endpoint, replay a request, or trigger a retry does not have to work hard to do that.
This is the shape MITRE files as CWE-841 (Improper Enforcement of Behavioral Workflow), and the tell is a multi-step process whose steps are separate endpoints or handlers, with the ordering enforced only by the UI that calls them in order. The fix is a precondition at each step asserting the prior state - order.state == PAID - rather than one check at the entry point of the flow, because the entry point is what an attacker skips.
A dispatch with no default branch
// VULNERABLE - pseudo-code
switch message.type:
case ORDER: handle_order(message)
case REFUND: handle_refund(message)
// no default - an unrecognised type falls through, and the caller is told it worked
Why this is vulnerable: the absent case does not mean nothing happens; it means the caller is told it worked. A message type nobody wrote a handler for is by definition one the design did not anticipate, so falling through means an unanticipated input has been accepted and acknowledged. Where the caller treats that acknowledgement as proof the message was processed, the record and the reality diverge with nothing logged.
The example above is CWE-431 (Missing Handler), and the default branch that rejects is the whole of its fix. The tell is visible in the source: either a dispatch with no default, or a default that logs without returning.
Its sibling CWE-430 (Deployment of Wrong Handler) is not fixed by that, and the two are worth keeping apart because a rejecting default branch is a satisfying enough answer that it gets applied to both. In CWE-430 a handler exists and runs; the object was routed to the wrong one. MITRE's own examples are a servlet that serves a .JSP file's source instead of executing it, and a type determined from the content in contradiction of the type that was explicitly declared. A default branch never fires, because the dispatch matched. MITRE's mitigations are to check the type before interpreting the object and to reject inconsistent types: a file declaring .gif whose bytes are a script calls for refusing the object rather than for a default branch. Where that object is an upload, CWE-434 has the detail.
Secure Patterns
// SECURE - pseudo-code
function ship_order(order_id, actor):
order = load(order_id)
if order.state != PAID: // precondition checked here, not upstream
raise IllegalStateError
if not actor.may_ship(order):
raise NotAuthorized
order.state = SHIPPED // one method changes the state
mark_shipped(order)
switch message.type:
case ORDER: handle_order(message)
case REFUND: handle_refund(message)
default: reject_and_log(message) // unanticipated input is not success
Why this works: every route into the protected operation passes the same precondition, because the precondition lives at the operation rather than at the entry point a legitimate caller happens to use. Reaching it by an unintended route fails the same way as skipping the step openly. Confining the state change to one method makes the number of ways to reach a state equal to the number of methods that set it, which is a number someone can read. And a default branch that rejects turns the inputs nobody anticipated from silent acceptances into logged failures.
What Belongs on Another Page
A return value that is never inspected is the other way execution continues past a decision nobody made, and it is not a CWE-691 descendant - it is CWE-252, under CWE-754 and the CWE-703 pillar. It is worth naming here because findings for it arrive filed against CWE-691 often: the observable symptom is the same, and the remediation is not.
It is also the reason grepping for the name of a validation function is a poor way to confirm one is enforced. What has to be traced is the result: does a false answer lead anywhere other than the next line? The pattern arrives most often through a refactor, where a function that used to throw was changed to return a status and one of its callers was not updated - so the call site is unchanged, still compiles, and the behaviour it relied on has moved into a value nobody reads. A function whose result carries a security decision is better off throwing, since an ignored exception is a deliberate act and an ignored return value is an omission. Take the rest from CWE-252.
Two others route away from here as well: an exception escaping the frame meant to contain it is CWE-248, which is a genuine CWE-691 descendant by way of CWE-705 but has its own page; and a check that is correct until another thread changes the state under it is CWE-362, under CWE-662.