A06 Insecure Design
A perfect implementation of a bad design is still a vulnerability. Security has to be asked for before the first line of code, as a requirement, not found afterwards as a bug.
What it is
Insecure design is a weakness in how a feature is conceived: a missing control, a trust assumption that does not hold, or a business flow that can be abused as written. It differs from an implementation bug. A bug is code that does not do what the design says; an insecure design is code that does exactly what the design says, and the design is the problem.
:2025 keeps it as A06. The fix is a practice more than a patch: early, write abuse cases next to use cases, and reuse secure design patterns.
How the attack works
- A password reset that asks "what was your first pet?" lets anyone who can research you take over the account.
- A coupon endpoint with no limit per customer lets a script drain the promotion in minutes.
- A ticketing flow that holds seats without expiry lets a bot lock the whole venue.
- A multi-step checkout that trusts the price sent in the final step lets users pay what they like.
- A "forgot username" form that reveals whether an e-mail is registered enables account enumeration.
Vulnerable design, in code
The code below is clean and bug-free. The design lets an attacker try unlimited promo codes and trusts a client-supplied price.
@app.post("/api/checkout")
@login_required
def checkout():
data = request.get_json()
total = data["total"] # price decided by the client
code = data.get("promo")
if code and Promo.query.filter_by(code=code).first():
total = total * 0.5 # unlimited attempts, unlimited reuse
return charge(current_user, total)The fix
Design controls into the flow: the server computes the price from trusted data, a promo code is single-use per account and attempts are rate limited, and the limits are business rules written down before coding. The "enumerate promo codes" produced the requirement.
@app.post("/api/checkout")
@login_required
@rate_limit("10/minute", key=lambda: current_user.id)
def checkout():
cart = Cart.for_user(current_user) # server-side source of truth
total = cart.total() # prices come from the catalog
code = request.get_json().get("promo")
if code:
promo = Promo.redeem(code, user=current_user) # single use per user, atomic
if promo is None:
abort(400, "invalid promo")
total = promo.apply(total)
return charge(current_user, total)Prove the fix
A design control needs a test written from the abuse case, not from the happy path. Each abuse case you listed becomes an automated test: the client cannot set the price, a code cannot be reused, and a burst of attempts is throttled.
def test_client_cannot_choose_the_price(client, alice, cart_of_100):
r = client.post("/api/checkout", json={"total": 1}, auth=alice)
assert charged_amount(r) == 100
def test_promo_is_single_use(client, alice, promo_half_off):
assert client.post("/api/checkout", json={"promo": promo_half_off}, auth=alice).status_code == 200
assert client.post("/api/checkout", json={"promo": promo_half_off}, auth=alice).status_code == 400
def test_promo_guessing_is_throttled(client, alice):
codes = [client.post("/api/checkout", json={"promo": f"TRY{i}"}, auth=alice).status_code for i in range(30)]
assert 429 in codesAs an attacker I want to guess promo codes so that I get discounts I should not.
-> control: rate limit per account and per IP, codes are long and random, single use
-> test: test_promo_guessing_is_throttled
As a customer I want to change the price in the request so that I pay less.
-> control: price is computed on the server from the catalog
-> test: test_client_cannot_choose_the_priceChecklist
- Threat model new features before building: what are we protecting, who attacks it, what stops them ( is a fine start).
- Write abuse cases next to use cases and turn each into a requirement and a test.
- Reuse vetted patterns (authentication, , secure session handling) instead of inventing them.
- Separate tenants and trust levels by design; limit resource use per user.
- Use a standard such as to turn "be secure" into checkable requirements.
- Revisit the model when the design changes, not once per year.
connections on the map
- A06 Insecure Designdefines the rules forA01 Broken Access Control
- A06 Insecure Designsets the recovery flow ofA07 Authentication Failures
updated on 14/10/2026 · sources to go deeper · report an error
in other domains
- Threat modeling: think like the attacker before codingappsecthe method that finds the flaw before the code exists
Study before
test what you learned
Loading questions…