iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

A04 Insecure Design

A04:2021 — Insecure Design — covers flaws in the system design itself, not specific bugs. Missing threat modelling, no rate limits, no abuse cases, secret data in URLs — bugs you can’t patch with a code fix.

Threat model + secure design patterns

EXAMPLE
# A04 is the category bugs that aren't in the code — they're in the architecture.
# Code fixes don't help; you have to redesign.

# === 1. Missing threat modelling ===
#
# Symptom: features ship without anyone asking 'what could go wrong?'
# Fix: run a 30-60 min threat model session before non-trivial features.
# Cover STRIDE:
#   S - Spoofing
#   T - Tampering
#   R - Repudiation
#   I - Information disclosure
#   D - Denial of service
#   E - Elevation of privilege
# Output: a backlog of mitigations with owners.

# === 2. No rate limiting ===
#
# Symptom: login + signup + password-reset endpoints take unlimited requests.
# Effect: credential stuffing, brute-force, mass spam.
# Fix:
#   - Per-IP rate limit on the edge (Cloudflare, NGINX, app-level)
#   - Per-account limit (3 failed logins → 5 min lockout, 10 → 30 min)
#   - CAPTCHA / Turnstile after threshold
#   - Bot detection for automated traffic
#
# Express + express-rate-limit
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import Redis from 'ioredis';

const redis = new Redis();
const limiter = rateLimit({
    windowMs: 60_000,
    limit:    100,
    standardHeaders: true,
    legacyHeaders:   false,
    store: new RedisStore({
        sendCommand: (...args) => redis.call(...args),
    }),
});
app.use('/api', limiter);

const authLimiter = rateLimit({ windowMs: 60_000, limit: 5, keyGenerator: (req) => req.body.email });
app.post('/auth/login', authLimiter, handleLogin);
app.post('/auth/password-reset', authLimiter, handleReset);

# === 3. Sensitive data in URLs ===
#
# Symptom: GET /api/orders/?ssn=123456789, share URLs that include tokens, password reset URLs in query strings.
# Effect: URLs land in browser history, server logs, third-party analytics, Referer headers.
# Fix:
#   - Move sensitive data to POST body or Authorization header
#   - Use opaque IDs (UUID) for resources, never database row IDs
#   - Password reset tokens — single-use, short TTL, stored hashed on server
#   - Strip Referer with rel="noreferrer"

# === 4. Predictable identifiers ===
#
# Symptom: /orders/100, /orders/101 — auto-incrementing IDs
# Effect: IDOR — change the ID, see another user's data
# Fix:
#   - UUID / KSUID for resources, opaque to attackers
#   - But ALSO: server-side authz check on every read/write — never rely on opaque IDs alone
await db.orders.findOne({ id: orderId, userId: req.user.id });   // scoped

# === 5. Default credentials / weak passwords ===
#
# Symptom: admin/admin works on first install; no password complexity check
# Effect: every CVE list ever.
# Fix:
#   - Force password change at first login
#   - Check against HaveIBeenPwned at signup
#   - argon2id with OWASP 2024 params
#   - Optional MFA on privileged accounts

# === 6. Account enumeration ===
#
# Symptom: /login returns 'invalid password' vs 'user not found'; signup returns 'email already exists'
# Effect: attackers harvest valid emails for phishing
# Fix:
#   - Generic responses: 'Invalid email or password'
#   - Signup: 'If this email is new, we've sent a verification link'
#   - Same response time for valid vs invalid (constant-time)
#   - Rate-limit so probes are slow

# === 7. Business-logic flaws ===
#
# Symptom: feature works as designed but the design allows abuse
# Examples:
#   - Refunds: negative quantity → free items
#   - Voucher stacking: combine two 100%-off codes → infinite discount
#   - Race conditions: simultaneous balance check + transfer → double-spend
# Fix:
#   - Server-side validation of ALL business rules (never trust client)
#   - Idempotency keys on payment / mutation endpoints
#   - Database-level constraints (CHECK total >= 0)
#   - Optimistic locking for concurrent edits
#   - Pen test against agreed abuse cases

# === 8. No audit trail ===
#
# Symptom: who deleted that order? No idea.
# Fix:
#   - Append-only audit log for sensitive actions (delete, role change, payment, login)
#   - Include: actor, action, target, before / after state, IP, UA, request ID
#   - Forward to a SIEM that's tamper-resistant (separate from app DB)
await audit.log({
    actor:    req.user.id,
    action:   'order.cancel',
    target:   orderId,
    metadata: { reason },
    ip:       req.ip,
    ua:       req.headers['user-agent'],
    request_id: req.id,
    at:       new Date(),
});

# === 9. No segregation of environments ===
#
# Symptom: dev / staging / prod share the same DB; prod-like test data on staging
# Fix:
#   - Separate DBs, separate creds, separate cloud accounts where possible
#   - Synthetic data for non-prod
#   - Production data masking if you need real-shaped data

# === 10. Insecure defaults ===
#
# Symptom: feature flags default ON; new accounts have admin role; OAuth redirect URL is *
# Fix:
#   - Deny by default for new permissions / features
#   - Conservative new-account defaults; opt in to more access
#   - Explicit redirect URI allowlists

# === 11. No abuse-case testing ===
#
# Symptom: tests cover happy path; QA never asks 'what if I cheat?'
# Fix:
#   - Pen test before major launches
#   - Bug bounty — bound + rewarded
#   - Add abuse cases to the test plan (negative quantity, replay, race, role change attempts)
#   - Pre-prod scanning (OWASP ZAP, Nuclei, semgrep + custom rules)

# === 12. Security debt / no patching cadence ===
#
# Symptom: dependencies + base images last updated 2 years ago
# Fix:
#   - Renovate / Dependabot weekly
#   - Trivy / Snyk in CI for image + dep scanning
#   - Defined patch SLA: critical < 7 days, high < 30 days
#   - Asset inventory: what's running, what version, what owner

# === 13. Secret management ===
#
# Symptom: secrets in env vars, in .env files, in source
# Fix:
#   - KMS / Secrets Manager / Vault — fetch at startup or on demand
#   - Rotation schedule + automated rotation where possible
#   - Pre-commit gitleaks / trufflehog

# === 14. Logging that creates new risks ===
#
# Symptom: full PII / passwords / card data in logs
# Fix:
#   - Structured logging with allowed fields only
#   - Redact known sensitive patterns (cards, SSN, tokens)
#   - Restricted access to logs (role-based)

# === 15. Failure to design for rollback ===
#
# Symptom: a bad deploy can't be reversed without data loss
# Fix:
#   - Blue/green or canary deploys
#   - Database migrations split from deploys (deploy backward-compat, migrate, then clean up)
#   - Feature flags so you can disable bad features without redeploy

# === Mitigation patterns ===

# 1. Defence-in-depth — assume each layer can fail
# 2. Deny by default — explicit allow > implicit reject
# 3. Least privilege — every service / role gets minimum permissions
# 4. Fail safe — errors don't expose information; UI shows generic messages
# 5. Validate at every trust boundary — UI, API, service, DB
# 6. Use established libraries — battle-tested > rolled-your-own
# 7. Plan for compromise — incident response runbook, breach notification flow

# === Resources ===
# - OWASP ASVS — 'Application Security Verification Standard' — checklist of designs to enforce
# - OWASP SAMM — 'Software Assurance Maturity Model' — process improvement
# - NIST SSDF — Secure Software Development Framework
# - Microsoft SDL — Security Development Lifecycle

Why it matters

Insecure Design is the category you can’t fix with a code patch — you need a different architecture. Bake threat modelling, rate limits, abuse cases, and audit logs into the design phase; retrofit costs 10x at minimum.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// A04 Insecure Design — broken at the threat-model layer.
// Example: password reset that ALSO logs you in without verifying ownership.
// Fix: threat-model new features, write abuse-cases, add invariants.
Try it Yourself »

Discussion

Loading…