Safeguard
Application Security•

CWE-841 Improper Enforcement of Behavioral Workflow

When steps must happen in order and the server does not check, attackers skip or reorder them. CWE-841 explains workflow bypass and how to enforce state.

Safeguard Security Team
2 min read

CWE-841 describes a session in which an actor must perform more than one behavior, but the product does not ensure they happen in the required order. MITRE marks it as not language specific. Checkouts, multi step forms, login handshakes and protocol state machines are all affected.

How it happens

The intended order exists only in the interface. The server accepts whichever request arrives. By omitting steps or reordering them, an attacker can manipulate business logic or push the product into an invalid state.

MITRE's example: a file sharing protocol requires a username, then a password, before transfers. If the server accepts a password command followed by a transfer command with no username, the transfer might still proceed.

From the record: CVE-2007-3012 let an attacker reach portions of a restricted page by canceling out of a dialog. CVE-2011-0348 bypassed access and billing restrictions by sending traffic to an unrestricted destination before a restricted one.

# Vulnerable: payment confirmation is assumed, never checked
@app.post("/order/<id>/ship")
def ship(id):
    start_shipping(id)

What an attacker gains

MITRE says an attacker could cause the product to skip critical steps or perform them in the wrong order, bypassing intended business logic, which can sometimes have security implications.

How to find it

MITRE lists automated static analysis. Manual testing works best: draw the intended flow, then call later steps directly, skip steps, repeat them and reverse them.

Click through it at your own pace, or open it full screen.

How to fix it

The record gives no formal mitigations. Standard approach:

  1. Keep workflow state on the server and tie it to the session.
  2. Define allowed transitions and reject anything else.
  3. Check preconditions in every handler, not just the first step.
@app.post("/order/<id>/ship")
def ship(id):
    order = load(id)
    if order.state != "paid":
        abort(409)
    start_shipping(order)

Quick check

Is hiding the next button enough?

No. Attackers send requests directly. Every step must verify the earlier ones happened.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.