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.