Safeguard
Application Security•

ASVS 5.0 V2.3.1: Enforce Business Flow Step Order

ASVS 5.0 V2.3.1 (Level 1) requires flows to run in order with no skipped steps. Server side state machine code, plus tests that jump straight to the last step.

Safeguard Security Team
4 min read

ASVS 5.0 V2.3.1 requires that the application only processes business logic flows for the same user in the expected sequential order, with no skipped steps. It is a Level 1 requirement.

Why it matters

Multi step flows such as checkout, onboarding, password reset and approvals assume that step three follows step two. If the server does not check, an attacker calls the final endpoint directly. The classic results are orders confirmed without payment, identity checks skipped, and approvals granted without a reviewer.

This is a logic flaw, so scanners rarely find it. Someone has to think about the flow.

How to implement it

Keep the flow state on the server, tied to the user and the specific transaction, and let each step check it.

ALLOWED = {
    "cart": {"address"},
    "address": {"payment"},
    "payment": {"confirmed"},
}

def advance(order, user, target):
    if order.user_id != user.id:
        raise Forbidden()
    if target not in ALLOWED.get(order.state, set()):
        raise Conflict("step not allowed from " + order.state)
    order.state = target
    order.save()

Do not put the state in a hidden field, a cookie value the user can edit, or a client side variable. If a token carries state, sign it and also check it against the stored record. Make the confirm step check the facts itself, for example that a successful payment record exists, instead of trusting that the previous step ran.

Step changes should be atomic, so two parallel requests cannot both move the order forward in a conflicting way.

How to test it

Map each flow and its endpoints. Then, with a fresh account, call the last endpoint first. Call step four with data from another transaction. Replay step one after step three. Change the step number in the request. Repeat with two accounts to check that one user cannot advance the other's flow. Each attempt should be rejected, and the stored state should not change.

Designing the flow so skipping is impossible

The cleanest approach is to make the state machine the only way to change an order. Write the allowed transitions as data, as in the example, and route every endpoint through one function. Reviewers can then read the whole flow in a single place, and a new endpoint cannot add a shortcut without someone noticing.

Idempotency and replays

Steps should be safe to repeat. A user who refreshes the confirm page, or an attacker who replays the request, must not trigger the action twice. Store the transaction state and a unique reference, and make the confirm step return the existing result if it has already run. For anything that moves money, add an idempotency key supplied by the client and stored with the result.

Concurrency

Two requests for the same order can arrive at once. Use a transaction with a row lock, or an update that includes the expected current state:

UPDATE orders SET state = 'payment'
WHERE id = :id AND user_id = :user AND state = 'address';

If the update changes zero rows, the step was not allowed from the current state. This keeps the check and the change in one atomic statement.

Flows that cross sessions and devices

Password reset and email verification involve a link opened on another device. Tie the token to the user and the purpose, expire it quickly, and mark it used after the first success. A token for one flow should never be accepted in another.

Common mistakes

  • Enforcing order in the UI only.
  • Letting the final step trust a client sent flag such as paid=true.
  • Allowing a step to be repeated and applied twice.
  • Forgetting alternate routes, like a mobile API or admin path, that reach the same action.

Quick check

Where does the flow state belong?

On the server, linked to the user and the transaction, and checked on every step.

Does a signed token fully solve it?

Not alone. It proves the client did not edit the state, but the server should still confirm that preconditions, such as payment, are recorded.

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.