Skip to content
back to the map
concept
stage: design
3 min read

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.

OWASP Threat DragonMicrosoft Threat Modeling ToolSTRIDEOWASP ASVSOWASP SAMMabuse-case workshops

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.

checkout.pypython
@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.

checkout.pypython
@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.

test_checkout_abuse.py (pytest)python
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 codes
abuse-cases.md (lives next to the user stories)markdown
As 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_price

Checklist

  • 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.
OWASP Top 10The best-known reference list of web application security risks (A01 broken access control, A02 cryptographic failures, A03 injection...), revised every few years by the OWASP community. It is an educational map of risk categories, not a tool checklist or a certification.see in glossary →Threat modelA structured answer to four questions: what are we building, what can go wrong, what will we do about it, and did we do a good job.see in glossary →Abuse caseA user story written from the attacker's side. Each one produces a requirement and a test.see in glossary →STRIDEA mnemonic of six threat categories (Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege) used as a checklist when walking through a data flow diagram.see in glossary →Rate limitingLimit on requests per entity and time window. The choice of key (IP, account, token, ASN) matters as much as the limit value.see in glossary →OWASP ASVSverification standardApplication Security Verification Standard: a verifiable checklist, with three levels of rigor, for auditing an application or commissioning an audit. Where the Top 10 teaches, the ASVS audits.see in glossary →

connections on the map

updated on 14/10/2026 · sources to go deeper · report an error

in other domains

Study before

test what you learned

Loading questions…