The XSS Rat OWASP Lab by The XSS Rat
A06:2025 High

Insecure Design

Theory

Insecure Design entered the list in 2021 and sits at #6 in 2025. Unlike other categories that describe implementation flaws, insecure design means the security control was never built in the first place. You cannot patch your way out of insecure design — it requires re-architecting the feature. The question to ask: "If I assumed an attacker would use this feature, how would I design it differently?"

What Insecure Design Looks Like

  • Predictable secrets — reset tokens derived from timestamps, user IDs, or sequential counters instead of cryptographic randomness
  • No rate limiting on sensitive actions — login, OTP verification, and password reset endpoints have unlimited retries, allowing brute-force
  • Business logic flaws — the application never validates that a business rule holds (e.g., negative quantities, applying a coupon multiple times, gift card abuse)
  • Flat trust models — all authenticated users are treated identically regardless of context (IP, device, risk score)
  • No abuse cases considered — the design process only modelled happy-path scenarios, never adversarial ones

Predictable Reset Token — Vulnerable Design

# VULNERABLE — reset token is user_id + "0000"; entirely predictable
import random

def generate_reset_token(user_id: int) -> str:
    return str(user_id) + "0000"   # attacker knows user_id=1 (admin) → token is "10000"

# Also broken: time-based tokens
def generate_reset_token_v2(user_id: int) -> str:
    return str(user_id) + str(int(time.time()))[-4:]   # predictable within a ~10-second window

No Rate Limiting — Vulnerable Design

# VULNERABLE — no attempt counter, no lockout, no delay
@app.post("/verify-otp")
def verify_otp(otp: str = Form()):
    if otp == admin_otp:           # 4 digits = 10,000 possibilities
        return {"flag": FLAG}      # attacker enumerates 0000–9999 in < 1 minute
    return {"error": "wrong otp"}

# Brute-force attack:
for i in range(10000):
    r = requests.post("/verify-otp", data={"otp": f"{i:04d}"})
    if "flag" in r.text:
        print(f"OTP: {i:04d}")
        break

Fixed Design

import secrets
from datetime import datetime, timedelta

# FIXED — cryptographically random token with expiry
def generate_reset_token(user_id: int) -> str:
    token = secrets.token_urlsafe(32)    # 256 bits of randomness; unguessable
    token_store[token] = {
        "user_id": user_id,
        "expires": datetime.utcnow() + timedelta(minutes=15)   # short expiry window
    }
    return token

# FIXED — rate limiting on OTP with lockout after 5 attempts
attempt_count = {}

@app.post("/verify-otp")
def verify_otp(request: Request, otp: str = Form()):
    ip = request.client.host
    attempts = attempt_count.get(ip, 0)
    if attempts >= 5:
        raise HTTPException(429, "Too many attempts. Try again in 15 minutes.")
    if otp != admin_otp:
        attempt_count[ip] = attempts + 1
        return {"error": "wrong otp"}
    attempt_count.pop(ip, None)
    return {"flag": FLAG}

Real-World Examples

  • Instagram (2019) — Password reset via 6-digit SMS code had no rate limit; 1,000,000 requests/hour were possible. Account takeover at scale. Paid $30,000 bug bounty.
  • PayPal (2012) — Password reset flow revealed whether an email was registered in the system, enabling account enumeration for targeted phishing
  • Robinhood (2021) — 5 million email addresses and 2 million names exposed via a social-engineering attack on customer support — no design control to limit what support agents could export
  • Any "security question" system — "What is your mother's maiden name?" can be answered from LinkedIn/Facebook; fundamentally insecure design regardless of implementation quality

How to Fix — Checklist

  • Cryptographic randomness for all secrets — use secrets.token_urlsafe(32) (Python) or crypto.randomBytes(32) (Node). Never timestamps, IDs, or random
  • Rate limiting on every authentication/verification endpoint — max 5 attempts, exponential backoff or IP lockout, CAPTCHA after threshold
  • Short token expiry — password reset tokens expire in 15 minutes; OTPs expire in 30 seconds
  • Threat modelling during design — use STRIDE or abuse cases: for every feature, ask "how would an attacker misuse this?"
  • Single-use tokens — invalidate a reset/OTP token immediately after first use, even if still within the expiry window
Challenge 1

Predictable Password Reset Token

The password reset token is derived from the user ID and a known constant, making it predictable. Request a reset for admin and compute the token yourself.

Steps
Reset token = str(user_id) + '0000'. Admin's user_id is 1. So admin's token is 10000.
Challenge 2

No Rate Limiting on OTP

The 2-step verification OTP is a 4-digit number with no rate limiting. Brute-force all values from 0000–9999. The correct OTP for admin is seeded as 7391.

Steps
Try: for i in $(seq -w 0 9999); do curl -s -X POST /web/a06/verify-otp -d "otp=$i" | grep -q flag && echo $i && break; done