π OWASP Top 10 β A04:2021 Insecure Design
When Your Architecture Is the Vulnerability: A Deep Dive Into Everything That Goes Wrong Before a Single Line of Code Is Written
"The most expensive bug is the one born in the whiteboard session, not in the code editor."
>
This blog dissects A04:2021 β Insecure Design down to the atomic level. By the time you finish reading, you will never again say "we'll add security later."
π Table of Contents
- Introduction β Why Does This Category Exist?
- What Exactly Is Insecure Design?
- Insecure Design vs Insecure Implementation
- Real-World Attack Scenarios
- Technical Deep-Dive: CWE Mapping
- Threat Modeling β Your First Line of Defense
- Secure Design Patterns
- Anti-Patterns: What NOT to Do
- Defense-in-Depth Strategy
- Secure SDLC Integration
- Code Examples: Vulnerable vs Secure
- Tools & Resources
- The Ultimate Checklist
- Conclusion
πͺ Introduction β Why Does This Category Exist?
When OWASP refreshed its Top 10 list in 2021, it introduced a brand-new category: A04:2021 β Insecure Design. This was not an accident. It was an acknowledgment of a painful truth that the security community had been screaming about for years:
π΄ The majority of catastrophic vulnerabilities are not born in code β they are born in architecture.
Think of it this way: if you lay the foundation of a house crooked, no amount of beautiful paint on the walls will stop that house from collapsing. Software is exactly the same.
The Numbers Tell the Story
| Metric | Value |
|---|---|
| Incidence Rate (tested applications) | 3.00% |
| Mapped CWEs | 40 |
| Max Incidence Rate | 24.19% |
| Mapped CVEs | 262,407 |
| Average Weighted CVSS Score | 6.46 |
These numbers reveal something critical β insecure design is endemic. It is not a rare edge case. It is the norm in most organizations that do not deliberately invest in secure architecture.
The Cost of Fixing Bugs by Phase
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β RELATIVE COST TO FIX β
βββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββ€
β Requirements β π° x1 β
β Design β π°π° x5 β
β Development β π°π°π° x10 β
β Testing β π°π°π°π° x20 β
β Production β π°π°π°π°π°π°π°π°π°π° x100 β
βββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββ
Source: IBM Systems Sciences Institute / NISTA design-level flaw found in production can cost 100x more to fix than if it had been caught during the requirements phase. That is not a typo. One hundred times.
π§ What Exactly Is Insecure Design?
Insecure Design is a broad category representing different weaknesses expressed as missing or ineffective control design. It refers to flaws in the fundamental architecture and design decisions of a system β decisions made (or not made) before any code is written.
In Plain English
Insecure Design = "We didn't think about that"
+ "Who would even do that?"
+ "We'll add security later"
+ "That's an edge case, nobody will try it"The Official OWASP Definition
"An insecure design cannot be fixed by a perfect implementation as by definition, needed security controls were never created to defend against specific attacks."
Read that last part again: "needed security controls were never created." The defense mechanism was never designed to exist. You cannot patch something that was never built.
The Building Analogy
| Scenario | Category | Fix |
|---|---|---|
| π¦ You designed a bank with a vault, cameras, laser sensors, and armed guards β but the lock on the front door turned out to be defective | Implementation Bug | Replace the lock. Done. |
| ποΈ You designed a bank with no vault, no cameras, no sensors, and a permanently open back door | Insecure Design | Tear the building down and redesign it from scratch. |
The second scenario is exponentially more expensive and fundamentally more dangerous.
What Insecure Design Is NOT
Let's be crystal clear about the boundaries:
β Insecure Design is NOT:
- A specific vulnerability like SQL Injection or XSS
- A coding mistake
- A misconfiguration
- Something that can be caught by a SAST scanner
β
Insecure Design IS:
- A missing security control that was never planned
- A flawed business logic that enables abuse
- An architecture that trusts untrusted input by design
- A system that fails open instead of failing secure
- The absence of rate limiting, MFA, or abuse prevention
because nobody thought to include themβοΈ Insecure Design vs Insecure Implementation β The Critical Distinction
These two concepts are confused constantly, even by experienced developers. Let's eliminate all ambiguity.
Side-by-Side Comparison
| Characteristic | Insecure Design | Insecure Implementation |
|---|---|---|
| When does it happen? | Architecture & design phase | Coding & configuration phase |
| Root Cause | Security requirements missing or inadequate | Security requirements exist but are incorrectly coded |
| How to fix | Redesign the architecture | Patch the code |
| How to detect | Threat modeling, design review, abuse case analysis | Code review, SAST, DAST, penetration testing |
| Cost to fix | π°π°π°π°π° (Extremely expensive) | π°π° (Relatively affordable) |
| Example | No password recovery mechanism exists | Password recovery uses MD5 hash |
| Can perfect code fix it? | β NEVER | β Yes |
The Golden Rule
β
Perfect Implementation + Weak Design = VULNERABLE SYSTEM (always)
π‘ Weak Implementation + Strong Design = FIXABLE SYSTEM (patch it)
β
Perfect Implementation + Strong Design = SECURE SYSTEM (the goal)This is the single most important takeaway: you cannot code your way out of a design flaw. No matter how clean, elegant, or well-tested your code is β if the architecture is fundamentally insecure, the system is compromised.
π Real-World Attack Scenarios
Let's walk through detailed, realistic scenarios that demonstrate how insecure design manifests in production systems.
π Scenario 1: Cinema Booking System β The Scalper Attack
Why is this Insecure Design and NOT an implementation bug?
The code works exactly as designed. The problem is that the design itself is flawed:
- β Mass reservation without payment was an architectural decision
- β No rate limiting was designed into the API
- β No bot detection was planned in the system architecture
- β No per-user/per-IP seat limit was specified in requirements
- β No abuse case analysis was performed during design
The Secure Design Alternative:
β
Maximum 4 seats per session/user
β
Reservation window: 5 minutes (not 15)
β
No seat lock until payment process begins ("soft hold" only)
β
CAPTCHA + device fingerprinting for all reservations
β
Behavioral analysis: flag accounts making rapid bulk reservations
β
Queue system for high-demand events
β
Proof-of-work challenge for API consumersπ Scenario 2: E-Commerce β Price Manipulation
π΄ INSECURE DESIGN:
Client sends:
POST /api/order
{
"product_id": 1337,
"quantity": 1,
"price": 0.01, β The CLIENT dictates the price!
"discount": 99, β The CLIENT dictates the discount!
"currency": "USD"
}
Server blindly trusts and processes the order at $0.01The developer didn't make a "coding mistake." The architecture decided that the client would send pricing information, and the server would trust it. This is a design decision.
β
SECURE DESIGN:
Client sends:
POST /api/order
{
"product_id": 1337,
"quantity": 1
"text-slate-500 font-normal italic">// Price is NEVER accepted from the client
"text-slate-500 font-normal italic">// Discount codes are validated server-side
}
Server-side logic:
1. Fetch product price from database
2. Validate discount code against server-side rules
3. Calculate total on the server
4. Verify calculated total matches expected range
5. Write audit log for every transaction
6. Flag transactions with unusual discount patternsπ Scenario 3: Password Recovery β Knowledge-Based Authentication
Why is this a design flaw?
- Security questions are static β your mother's maiden name never changes
- Answers are often publicly available on social media
- They can be brute-forced (limited answer space)
- The entire recovery mechanism is architecturally weak
The Secure Design:
β
Email-based recovery link with 15-minute expiry
β
SMS/TOTP OTP with rate limiting (max 3 attempts)
β
MFA challenge before allowing password reset
β
Pre-generated account recovery codes (printed and stored safely)
β
Identity verification through multiple independent factors
β
Notify the user on ALL registered channels when a reset is requestedπ Scenario 4: Credential Stuffing β Defenseless by Design
The server code may be perfectly written β parameterized queries, proper password hashing, clean error handling. But if the system was never designed to handle automated large-scale login attempts, it is insecure by design.
π Scenario 5: IDOR β A Design-Level Perspective
π΄ INSECURE DESIGN:
GET /api/users/1001/medical-records β User 1001's medical data
Attacker simply changes the ID:
GET /api/users/1002/medical-records β Someone else's medical data! πThis is an Insecure Design problem because:
- No authorization layer was designed β the principle "each user can only access their own data" was never embedded into the architecture
- Sequential integer IDs were chosen as the identifier scheme (a design decision) β making enumeration trivial
- No Access Control Matrix was ever drawn during the design phase
- No resource-level authorization policy was specified in the requirements
β
SECURE DESIGN:
1. Use UUIDs: GET /api/users/a8f3b2c1-4d5e.../medical-records
β Eliminates enumeration
2. Resource-level authorization on EVERY request:
β Token's user_id == requested resource's owner_id?
β Does the user have the required role/permission?
3. Consistent authorization enforcement:
β Centralized policy engine (OPA, Casbin, etc.)
β Every endpoint goes through the same authz middleware
4. Return 404 (not 403) for unauthorized access:
β Don't reveal that the resource exists
5. Comprehensive audit logging:
β Log every access attempt (success AND failure)π Scenario 6: E-Commerce β Coupon Abuse (Business Logic Flaw)
π΄ INSECURE DESIGN β No abuse case was ever considered:
1. User applies coupon code "WELCOME50" (-50% discount)
2. User places order β pays $50 instead of $100
3. User cancels the order β gets full $50 refund
4. User applies the SAME coupon again β pays $50
5. Repeat infinitely
Worse:
6. User discovers coupon codes are sequential: WELCOME50, WELCOME51, WELCOME52...
7. User brute-forces valid coupon codes
8. User shares valid codes on public forums
β
SECURE DESIGN:
1. Coupon usage tracked per-user in database (one-time use flag)
2. Coupon bound to specific user segment (new users only, etc.)
3. Refund does NOT automatically re-enable coupon β manual review required
4. Coupon codes use cryptographically random strings (not sequential)
5. Anomaly detection: flag accounts with excessive coupon usage
6. Coupon issuance rate limiting
7. Server-side validation of ALL coupon rules on every applicationπΊοΈ Technical Deep-Dive: CWE Mapping
A04:2021 maps to 40 CWEs. Here are the most critical ones, organized by theme:
Detailed CWE Breakdown
πΈ CWE-209: Generation of Error Message Containing Sensitive Information
# π΄ VULNERABLE β Stack trace and internal details exposed to user
@app.route('/login', methods=['POST'])
def login():
try:
user = db.execute(
f"SELECT * FROM users WHERE email='{request.json['email']}'"
)
if not verify_password(request.json['password'], user.password_hash):
return {
"error": f"Wrong password for user {request.json['email']} "
f"in table 'users' (schema: public, db: prod_userdb)"
}, 401 # β Leaks table name, schema, database name
except Exception as e:
return {"error": str(e)}, 500
# β Leaks full stack trace, SQL errors, internal paths
# β
SECURE β Generic messages to user, detailed logs internally
@app.route('/login', methods=['POST'])
def login():
try:
user = authenticate(request.json.get('email'), request.json.get('password'))
if not user:
return {"error": "Invalid email or password"}, 401
# β Same message whether email exists or not (prevents enumeration)
except Exception as e:
error_id = str(uuid.uuid4())
logger.error(f"Login error [{error_id}]: {e}", exc_info=True)
# β Full details go to internal logs only
return {
"error": "An unexpected error occurred",
"reference": error_id
# β User gets a reference ID to report, not the actual error
}, 500πΈ CWE-602: Client-Side Enforcement of Server-Side Security
// π΄ VULNERABLE β Authorization enforced ONLY in JavaScript
function deleteUser(userId) {
if (currentUser.role === 'admin') { // β Client-side check only
fetch(`/api/users/${userId}`, { method: 'DELETE' });
} else {
alert('Access denied!');
}
}
// Attacker opens DevTools, runs:
// fetch('/api/users/1002', { method: 'DELETE' })
// β Server happily deletes the user because it has NO authorization check# β
SECURE β Authorization enforced on the SERVER
@app.route('/api/users/<user_id>', methods=['DELETE'])
@require_auth # β Authentication middleware
def delete_user(user_id):
# Server-side authorization check
if not current_user.has_permission('user:delete'):
audit_log.warning(
f"Unauthorized delete attempt by user {current_user.id} "
f"for target {user_id}"
)
abort(403) # β Server says NO
# Additional check: prevent self-deletion
if user_id == current_user.id:
abort(400, "Cannot delete your own account")
# Proceed with deletion
user = User.query.get_or_404(user_id)
user.soft_delete() # β Soft delete, not hard delete (another design decision)
db.session.commit()
audit_log.info(f"User {user_id} deleted by admin {current_user.id}")
return {"message": "User deleted"}, 200πΈ CWE-799: Improper Control of Interaction Frequency
Design Flaw: No rate limiting on OTP verification endpoint
π΄ VULNERABLE:
POST /api/verify-otp
{ "phone": "+1234567890", "otp": "1234" }
β 4-digit OTP = only 10,000 possible combinations
β No rate limiting = attacker tries all 10,000 in ~30 seconds
β Account takeover guaranteed
β
SECURE DESIGN:
1. 6-digit OTP (1,000,000 combinations)
2. Max 3 attempts per OTP
3. OTP expires after 5 minutes
4. 30-second cooldown between OTP requests
5. Account lockout after 5 failed OTPs in 1 hour
6. CAPTCHA after 2 failed attempts
7. Notify user of failed verification attemptsπΈ CWE-840: Business Logic Errors
π― Threat Modeling β Your First Line of Defense
Threat Modeling is the most powerful antidote to insecure design. It forces you to think like an attacker before any code is written.
The STRIDE Model
The Threat Modeling Process β Step by Step
Data Flow Diagram (DFD) β E-Commerce Example
π Key Principle: Every time data crosses a trust boundary, it MUST be validated, sanitized, and authorized. This is a design decision, not an implementation detail.
DREAD Risk Scoring Table
| Threat | Damage | Reproducibility | Exploitability | Affected Users | Discoverability | Score |
|---|---|---|---|---|---|---|
| Credential Stuffing (no rate limit) | 8 | 10 | 9 | 9 | 8 | 8.8 π΄ |
| Price Manipulation (client-side pricing) | 9 | 10 | 7 | 6 | 6 | 7.6 π |
| IDOR on Medical Records | 10 | 9 | 8 | 7 | 5 | 7.8 π΄ |
| Error Message Information Leak | 3 | 10 | 10 | 2 | 9 | 6.8 π‘ |
| Coupon Abuse (business logic) | 7 | 10 | 8 | 4 | 7 | 7.2 π |
| Unlimited File Upload (no size/type check) | 8 | 9 | 7 | 8 | 6 | 7.6 π |
Scoring Guide: (1 = Low, 10 = High)
- π΄ 7.0+ = Critical β Fix immediately during design
- π 5.0β6.9 = High β Must address before development begins
- π‘ 3.0β4.9 = Medium β Plan mitigation
- π’ < 3.0 = Low β Accept or monitor
ποΈ Secure Design Patterns
Pattern 1: Defense in Depth
Never rely on a single security control. Layer your defenses so that if one layer fails, others still protect the system.
Pattern 2: Zero Trust Architecture
π Principle: "Never trust, always verify"
For EVERY request, regardless of origin:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1. Verify Identity β WHO is making this request? β
β 2. Verify Device β WHAT device are they using? β
β 3. Verify Context β WHERE, WHEN, and WHY? β
β 4. Enforce Least Privilege β Minimum permissions β
β 5. Assume Breach β Monitor and log everything β
β 6. Verify Explicitly β Don't trust cached authz β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Traditional Security: "Inside the firewall = trusted" β
Zero Trust: "Nothing is trusted, ever, period" β
Pattern 3: Fail Secure (Not Fail Open)
# βββββββββββββββββββββββββββββββββββββββββββββββββββ
# π΄ FAIL OPEN β If auth service is down, allow everything
# βββββββββββββββββββββββββββββββββββββββββββββββββββ
def check_permission(user, resource):
try:
return authorization_service.check(user, resource)
except ServiceUnavailableError:
return True # β If auth service is down, GRANT ACCESS TO EVERYTHING π±
# "We can't let auth service downtime affect user experience!"
# βββββββββββββββββββββββββββββββββββββββββββββββββββ
# β
FAIL SECURE β If auth service is down, deny everything
# βββββββββββββββββββββββββββββββββββββββββββββββββββ
def check_permission(user, resource):
try:
return authorization_service.check(user, resource)
except ServiceUnavailableError:
logger.critical(
"Authorization service is DOWN! "
"All requests are being denied as a security precaution."
)
alert_ops_team(severity="P1")
return False # β If auth service is down, DENY ALL ACCESS π
# "Security is more important than availability in this context"
except Exception as e:
logger.error(f"Unexpected authorization error: {e}")
return False # β Default deny for ANY unexpected errorPattern 4: Principle of Least Privilege (PoLP)
Pattern 5: Secure Defaults
# β
Everything should be CLOSED/RESTRICTED by default
# Access is GRANTED explicitly, never implicitly
security:
cors:
allowed_origins: [] # Default: NO origins allowed
allowed_methods: ["GET"] # Default: read-only
authentication:
required: true # Default: auth REQUIRED
mfa_enabled: true # Default: MFA ON
password_min_length: 12 # Default: 12 characters
password_require_complexity: true
session:
timeout_minutes: 15 # Default: 15-minute timeout
secure_cookie: true # Default: HTTPS only
same_site: "Strict" # Default: strict SameSite
http_only: true # Default: no JS access to cookies
rate_limiting:
enabled: true # Default: rate limiting ON
requests_per_minute: 60 # Default: 60 req/min
login_attempts_per_hour: 10 # Default: 10 login attempts/hour
file_upload:
enabled: false # Default: uploads DISABLED
max_size_mb: 5 # Default: 5MB max
allowed_types: [] # Default: NO file types allowed
logging:
audit_trail: true # Default: audit ON
log_auth_events: true # Default: log all auth events
headers:
x_content_type_options: "nosniff"
x_frame_options: "DENY"
strict_transport_security: "max-age=31536000; includeSubDomains"
content_security_policy: "default-src 'self'"Pattern 6: Separation of Duties
π Critical operations should require MULTIPLE actors:
Example: Financial Transfer > $10,000
Step 1: Initiator (Finance Analyst) creates the transfer request
Step 2: Reviewer (Finance Manager) approves the transfer
Step 3: System verifies both actors are different people
Step 4: System verifies both actors have the required roles
Step 5: Transfer is executed
Step 6: Both actors are notified of completion
Step 7: Audit trail records all actors, timestamps, and actions
β One person should NEVER be able to:
- Create AND approve a transaction
- Write code AND deploy it to production
- Create a user AND assign admin privilegesπ« Anti-Patterns: What NOT to Do
Anti-Pattern 1: Security by Obscurity
π΄ "Nobody knows our API endpoint, so we don't need authentication"
π΄ "This port isn't commonly scanned, so we don't need to secure it"
π΄ "Our source code is proprietary, so nobody can find the vulnerabilities"
π΄ "We use a custom encryption algorithm that nobody knows about"
β
Kerckhoffs' Principle (1883):
"A cryptographic system should be secure even if everything about
the system, except the key, is public knowledge."
Applied broadly: Your system should be secure even if the attacker
has your source code, knows your architecture, and understands
every design decision you made.Anti-Pattern 2: Trusting the Client
π΄ Storing price in a hidden HTML field
π΄ Performing authorization checks in JavaScript only
π΄ Relying on client-side input validation without server-side validation
π΄ Using a "disabled" button as a security control (DevTools β enable β click)
π΄ Sending user role in a cookie or local storage and trusting it
π΄ Using client-side feature flags for security-sensitive features
β
THE GOLDEN RULE:
"All input from the client is HOSTILE until proven otherwise."
The client (browser, mobile app, API consumer) is ALWAYS under
the attacker's control. EVERY input, header, cookie, and parameter
must be validated, sanitized, and authorized on the SERVER.Anti-Pattern 3: "Happy Path Only" Design
Secure design requires thinking about:
- π Abuse cases β How will attackers misuse this feature?
- π₯ Failure modes β What happens when things go wrong?
- π Edge cases β What happens with boundary values?
- β‘ Race conditions β What happens with concurrent requests?
- π Scale attacks β What happens with 1 million requests?
Anti-Pattern 4: Monolithic Authorization
π΄ WRONG: A single "isAdmin" boolean controls everything
if (user.isAdmin) {
"text-slate-500 font-normal italic">// Can do LITERALLY ANYTHING:
"text-slate-500 font-normal italic">// - Delete all users
"text-slate-500 font-normal italic">// - Access financial records
"text-slate-500 font-normal italic">// - Modify system configuration
"text-slate-500 font-normal italic">// - Export all data
"text-slate-500 font-normal italic">// - Impersonate other users
}
β
CORRECT: Granular, resource-level permission system
Permission Matrix:
ββββββββββββββββββββββββ¬βββββββ¬ββββββββ¬βββββββββ¬βββββββββββ¬ββββββββββββ
β Resource β Read β Write β Delete β Export β Admin β
ββββββββββββββββββββββββΌβββββββΌββββββββΌβββββββββΌβββββββββββΌββββββββββββ€
β Own Profile β β
β β
β β β β
β β β
β Team Members β β
β β β β β β β β β
β Department Data β β
β β
β β β β β β β
β Financial Reports β β β β β β β β β β β
β System Configuration β β β β β β β β β β β
β Audit Logs β β β β β β β β β β β
ββββββββββββββββββββββββ΄βββββββ΄ββββββββ΄βββββββββ΄βββββββββββ΄ββββββββββββAnti-Pattern 5: "We'll Add Security Later"
The Real Timeline:
Sprint 1: "Let's build the feature first, we'll add auth later"
Sprint 2: "We're behind schedule, security will have to wait"
Sprint 3: "The feature works, let's ship it"
Sprint 4: "We have new features to build, security can wait"
Sprint 5: "We have too much tech debt to refactor for security now"
Sprint 47: "We got breached"
Sprint 48: "Why didn't we add security earlier?"
Cost to add security in Sprint 1: π°
Cost to add security in Sprint 48: π°π°π°π°π°π°π°π°π°π°π‘οΈ Defense-in-Depth Strategy
Complete Defense Architecture
Why Each Layer Matters
If Layer 0 fails: β Attacker bypasses DNS/CDN protections
But Layer 1 (WAF) blocks malicious requests β
If Layer 1 fails: β Malicious traffic reaches your network
But Layer 2 (segmentation) limits blast radius β
If Layer 2 fails: β Attacker moves laterally in the network
But Layer 3 (identity) requires strong authentication β
If Layer 3 fails: β Attacker authenticates somehow
But Layer 4 (application) validates all input and enforces authz β
If Layer 4 fails: β Attacker exploits an application vulnerability
But Layer 5 (data) keeps data encrypted and tokenized β
If Layer 5 fails: β Attacker accesses raw data
But Layer 6 (monitoring) detects the breach in real-time β
Every layer must fail INDEPENDENTLY for a full compromise.π Secure SDLC Integration
Secure Design is not a one-time activity β it must be woven into every phase of the Software Development Lifecycle.
Phase-by-Phase Activities
π Phase 1: Requirements
Security Requirements Gathering:
β‘ Functional security requirements defined?
β "Users MUST authenticate with MFA before accessing financial data"
β‘ Abuse cases / misuse cases written?
β "An attacker WILL attempt to brute-force the login endpoint"
β "A malicious user WILL try to access other users' data"
β "An insider WILL attempt to exfiltrate customer data"
β‘ Compliance requirements mapped?
β GDPR (data privacy), PCI-DSS (payment), HIPAA (health), SOC 2
β‘ Data classification completed?
β Public | Internal | Confidential | Restricted
β‘ Security SLAs defined?
β "Critical vulnerabilities must be patched within 24 hours"
β "99.9% uptime with DDoS protection"
β‘ Privacy requirements specified?
β Data retention periods, right to deletion, consent managementπ Phase 2: Design
Secure Design Activities:
β‘ Threat Modeling conducted? (STRIDE / PASTA / LINDDUN)
β‘ Attack surface analysis performed?
β‘ Security architecture review completed by security team?
β‘ Trust boundaries clearly identified and documented?
β‘ Secure design patterns applied?
β‘ Failure modes analyzed? (Fail secure, not fail open)
β‘ Authentication & authorization architecture designed?
β‘ Data flow diagrams created with security annotations?
β‘ Third-party integration risks assessed?
β‘ Cryptographic choices documented and justified?π» Phase 3: Development
Secure Development Practices:
β‘ Secure coding standards adopted and enforced?
β‘ SAST tools integrated into CI/CD pipeline?
β‘ Pre-commit hooks for secret detection? (git-secrets, detect-secrets)
β‘ Dependency scanning active? (Snyk, Dependabot)
β‘ Security-focused code reviews conducted?
β‘ Secret management solved? (HashiCorp Vault, AWS Secrets Manager)
β‘ Parameterized queries / ORM used? (No string concatenation for SQL)
β‘ Output encoding applied in all templates?
β‘ Security unit tests written for auth/authz logic?π§ͺ Phase 4: Testing
Security Testing Activities:
β‘ DAST scan completed?
β‘ Penetration testing performed by qualified testers?
β‘ Business logic testing done? (abuse cases from Phase 1)
β‘ Authentication bypass testing?
β‘ Authorization bypass testing? (horizontal & vertical)
β‘ Race condition testing? (TOCTOU flaws)
β‘ Fuzz testing for input parsing?
β‘ API security testing? (OWASP API Security Top 10)
β‘ Session management testing?
β‘ Error handling and information leakage testing?π» Code Examples: Vulnerable vs Secure
Example 1: User Registration β Multi-Layered Defense
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# π΄ INSECURE DESIGN β No rate limiting, no validation, no defense
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@app.route('/api/register', methods=['POST'])
def register():
email = request.json.get('email')
password = request.json.get('password')
# No rate limiting β a bot can create 100,000 accounts per hour
# No CAPTCHA β automated registration is trivial
# No email verification β fake emails accepted
# No password policy β "1" is a valid password
# No duplicate check timing safety β email enumeration possible
user = User(email=email, password=hash_password(password))
db.session.add(user)
db.session.commit()
return {"message": "Registered successfully"}, 201
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# β
SECURE DESIGN β Defense in depth at every step
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
import re
import uuid
import bcrypt
import secrets
from datetime import datetime, timedelta
limiter = Limiter(app, key_func=get_remote_address)
# Design Decisions documented:
# 1. IP-based rate limiting (5 registrations per hour per IP)
# 2. CAPTCHA verification before processing
# 3. Strict email format validation (server-side)
# 4. Strong password policy (12+ chars, complexity, breach check)
# 5. Timing-safe duplicate check (prevent email enumeration)
# 6. Email verification required before account activation
# 7. Comprehensive audit logging
# 8. No sensitive data in responses
@app.route('/api/register', methods=['POST'])
@limiter.limit("5 per hour") # Design: max 5 registrations per IP per hour
def register():
data = request.json or {}
# ββ Step 1: CAPTCHA Verification ββ
captcha_token = data.get('captcha_token')
if not captcha_token or not verify_captcha(captcha_token):
audit_log.warning(f"Failed CAPTCHA from {request.remote_addr}")
return {"error": "CAPTCHA verification failed"}, 400
# ββ Step 2: Input Validation (Server-Side, Whitelist) ββ
email = data.get('email', '').strip().lower()
password = data.get('password', '')
if not email or not re.match(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email
):
return {"error": "Invalid email format"}, 400
if len(email) > 254: # RFC 5321 max email length
return {"error": "Email address too long"}, 400
# ββ Step 3: Password Policy Enforcement ββ
password_errors = validate_password_strength(password)
if password_errors:
return {"error": "Password does not meet requirements",
"details": password_errors}, 400
# ββ Step 4: Breached Password Check (HaveIBeenPwned API) ββ
if is_password_breached(password):
return {
"error": "This password has been found in known data breaches. "
"Please choose a different password."
}, 400
# ββ Step 5: Timing-Safe Duplicate Check ββ
# Always take the same amount of time regardless of whether
# the email exists β prevents email enumeration attacks
processing_start = datetime.utcnow()
existing_user = User.query.filter_by(email=email).first()
# Ensure consistent response time (200-300ms)
elapsed = (datetime.utcnow() - processing_start).total_seconds()
if elapsed < 0.25:
time.sleep(0.25 - elapsed)
if existing_user:
# SAME response as success β attacker can't tell if email exists
return {
"message": "If this email is not already registered, "
"you will receive a verification email shortly."
}, 200
# ββ Step 6: Create Unverified User ββ
verification_token = secrets.token_urlsafe(32)
user = User(
id=str(uuid.uuid4()), # UUID, not sequential integer
email=email,
password_hash=bcrypt.hashpw(
password.encode(), bcrypt.gensalt(rounds=12)
).decode(),
is_verified=False,
verification_token=verification_token,
verification_expires=datetime.utcnow() + timedelta(hours=24),
created_ip=anonymize_ip(request.remote_addr), # Store anonymized IP
created_at=datetime.utcnow()
)
db.session.add(user)
db.session.commit()
# ββ Step 7: Send Verification Email ββ
send_verification_email(
to=email,
token=verification_token,
expires_in="24 hours"
)
# ββ Step 8: Audit Log ββ
audit_log.info(
f"Registration attempt | "
f"email_prefix={email[:3]}*** | "
f"ip={anonymize_ip(request.remote_addr)} | "
f"user_agent={request.headers.get('User-Agent', 'unknown')[:50]}"
)
# ββ Step 9: Same response regardless of outcome ββ
return {
"message": "If this email is not already registered, "
"you will receive a verification email shortly."
}, 200
def validate_password_strength(password: str) -> list:
"""Design Decision: Password policy enforcement"""
errors = []
if len(password) < 12:
errors.append("Password must be at least 12 characters long")
if len(password) > 128:
errors.append("Password must not exceed 128 characters")
if not re.search(r'[A-Z]', password):
errors.append("Password must contain at least one uppercase letter")
if not re.search(r'[a-z]', password):
errors.append("Password must contain at least one lowercase letter")
if not re.search(r'\d', password):
errors.append("Password must contain at least one digit")
if not re.search(r'[!@#$%^&*(),.?":{}|<>\-_=+\[\]\\;\'`~]', password):
errors.append("Password must contain at least one special character")
if re.search(r'(.)\1{2,}', password):
errors.append("Password must not contain 3 or more repeated characters")
return errorsExample 2: Secure File Upload Architecture
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# π΄ INSECURE DESIGN β Trusts everything from the client
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@app.route('/upload', methods=['POST'])
def upload():
file = request.files['file']
# Saves directly to web root with original filename
# No size check, no type check, no malware scan
# Path traversal possible: filename="../../etc/cron.d/backdoor"
file.save(f'/var/www/html/uploads/{file.filename}')
return {"url": f"https://example.com/uploads/{file.filename}"}
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# β
SECURE DESIGN β Defense at every step
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
import uuid
import magic # python-magic for MIME detection via magic bytes
from pathlib import Path
from werkzeug.utils import secure_filename
# Design Decisions:
ALLOWED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.pdf'}
ALLOWED_MIMES = {
'image/jpeg', 'image/png', 'image/gif', 'application/pdf'
}
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
UPLOAD_DIR = Path('/var/data/uploads') # OUTSIDE web root
QUARANTINE_DIR = Path('/var/data/quarantine')
@app.route('/api/files', methods=['POST'])
@require_auth
@limiter.limit("10 per hour")
def upload_file():
# ββ Step 1: Check if file exists in request ββ
file = request.files.get('file')
if not file or not file.filename:
return {"error": "No file provided"}, 400
# ββ Step 2: File size check (before reading entire file) ββ
file.seek(0, 2) # Seek to end
file_size = file.tell()
file.seek(0) # Seek back to beginning
if file_size == 0:
return {"error": "Empty file"}, 400
if file_size > MAX_FILE_SIZE:
return {
"error": f"File too large. Maximum size: "
f"{MAX_FILE_SIZE // 1024 // 1024}MB"
}, 400
# ββ Step 3: Extension whitelist check ββ
original_filename = secure_filename(file.filename)
extension = Path(original_filename).suffix.lower()
if extension not in ALLOWED_EXTENSIONS:
audit_log.warning(
f"Blocked file upload: extension '{extension}' "
f"from user {current_user.id}"
)
return {"error": "File type not allowed"}, 400
# ββ Step 4: MIME type verification (magic bytes, not Content-Type header) ββ
header_bytes = file.read(2048)
file.seek(0)
detected_mime = magic.from_buffer(header_bytes, mime=True)
if detected_mime not in ALLOWED_MIMES:
audit_log.warning(
f"MIME mismatch: claimed extension '{extension}', "
f"detected MIME '{detected_mime}' "
f"from user {current_user.id}"
)
return {"error": "File content does not match its extension"}, 400
# ββ Step 5: Generate safe filename (UUID-based) ββ
safe_filename = f"{uuid.uuid4().hex}{extension}"
quarantine_path = QUARANTINE_DIR / safe_filename
# ββ Step 6: Save to quarantine first ββ
file.save(str(quarantine_path))
# ββ Step 7: Malware scan (ClamAV) ββ
if not scan_for_malware(quarantine_path):
audit_log.critical(
f"MALWARE DETECTED in upload from user {current_user.id} | "
f"original_name: {original_filename}"
)
quarantine_path.unlink() # Delete infected file
alert_security_team(
event="malware_upload",
user_id=current_user.id,
filename=original_filename
)
return {"error": "File rejected by security scan"}, 400
# ββ Step 8: Strip metadata (EXIF data can contain GPS, device info) ββ
strip_metadata(quarantine_path)
# ββ Step 9: Move from quarantine to permanent storage ββ
final_path = UPLOAD_DIR / safe_filename
quarantine_path.rename(final_path)
# ββ Step 10: Re-encode images (neutralize image-based attacks) ββ
if detected_mime.startswith('image/'):
reencode_image(final_path)
# ββ Step 11: Database record ββ
upload_record = FileUpload(
id=str(uuid.uuid4()),
user_id=current_user.id,
original_name=original_filename[:255],
stored_name=safe_filename,
mime_type=detected_mime,
size_bytes=file_size,
sha256_hash=compute_sha256(final_path),
uploaded_at=datetime.utcnow(),
ip_address=anonymize_ip(request.remote_addr)
)
db.session.add(upload_record)
db.session.commit()
# ββ Step 12: Audit log ββ
audit_log.info(
f"File uploaded | id={upload_record.id} | "
f"user={current_user.id} | "
f"size={file_size} | mime={detected_mime}"
)
# ββ Step 13: Return file access URL (via API, not direct path) ββ
return {
"id": upload_record.id,
"url": f"/api/files/{upload_record.id}",
# β Never expose the actual file path!
# File is served through the API with authorization checks
"size": file_size,
"type": detected_mime
}, 201Example 3: API Authorization β Resource-Level Access Control
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# π΄ INSECURE DESIGN β No authorization, IDOR vulnerability
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@app.route('/api/invoices/<invoice_id>')
def get_invoice(invoice_id):
# No authentication check
# No authorization check
# Sequential IDs allow enumeration
invoice = Invoice.query.get(invoice_id)
return jsonify(invoice.to_dict())
# Attacker: GET /api/invoices/1, 2, 3, 4... β dumps all invoices
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# β
SECURE DESIGN β Multi-layered authorization
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@app.route('/api/invoices/<invoice_id>')
@require_auth # Layer 1: Authentication
@require_permission('invoice:read') # Layer 2: Role-based permission
def get_invoice(invoice_id):
# ββ Layer 3: Input validation ββ
if not is_valid_uuid(invoice_id):
return {"error": "Invalid invoice ID format"}, 400
# ββ Layer 4: Fetch resource ββ
invoice = Invoice.query.get(invoice_id)
if not invoice:
return {"error": "Invoice not found"}, 404
# ββ Layer 5: Resource-level authorization ββ
if not can_access_invoice(current_user, invoice):
# Log the unauthorized access attempt with full context
audit_log.warning(
f"UNAUTHORIZED ACCESS ATTEMPT | "
f"user={current_user.id} (role={current_user.role}) | "
f"attempted_resource=invoice:{invoice_id} | "
f"resource_owner={invoice.owner_id} | "
f"ip={request.remote_addr}"
)
# Design Decision: Return 404, not 403
# Returning 403 confirms the resource EXISTS
# Returning 404 reveals nothing about resource existence
return {"error": "Invoice not found"}, 404
# ββ Layer 6: Data filtering based on user's role ββ
# Different users see different fields
response_data = invoice.to_filtered_dict(
requesting_user=current_user,
fields_policy=get_field_policy(current_user.role)
)
# ββ Layer 7: Audit log (successful access) ββ
audit_log.info(
f"Invoice accessed | invoice={invoice_id} | "
f"user={current_user.id} | role={current_user.role}"
)
return jsonify(response_data), 200
def can_access_invoice(user, invoice) -> bool:
"""
Resource-level authorization policy.
Design Decision: Authorization rules are centralized here,
not scattered across multiple endpoints.
"""
# Rule 1: Owner can always access their own invoices
if invoice.owner_id == user.id:
return True
# Rule 2: Finance managers can access invoices in their department
if (user.has_role('finance_manager')
and invoice.department_id == user.department_id):
return True
# Rule 3: C-level executives can access all invoices (with logging)
if user.has_role('executive'):
audit_log.info(
f"Executive access | user={user.id} | invoice={invoice.id}"
)
return True
# Rule 4: External auditors can access invoices in their audit scope
if (user.has_role('external_auditor')
and invoice.id in get_audit_scope(user.id)):
return True
# Default: DENY (fail secure)
return Falseπ§° Tools & Resources
Threat Modeling Tools
| Tool | Type | Cost | Best For |
|---|---|---|---|
| Microsoft Threat Modeling Tool | Desktop App | Free | STRIDE-based DFD modeling with automated threat generation |
| OWASP Threat Dragon | Web / Desktop | Free (Open Source) | Lightweight, developer-friendly threat modeling |
| IriusRisk | SaaS Platform | Paid (Enterprise) | Automated threat modeling at scale, compliance mapping |
| Threagile | CLI Tool | Free (Open Source) | Threat model as code (YAML-based), CI/CD integration |
| draw.io / diagrams.net | Web App | Free | General-purpose DFD and architecture diagrams |
| CAIRIS | Web App | Free (Open Source) | Risk analysis and security requirements management |
Security Design Review Frameworks
| Framework | Purpose | Link |
|---|---|---|
| OWASP ASVS v4.0 | Application Security Verification Standard β 286 security requirements | owasp.org/asvs |
| OWASP SAMM | Software Assurance Maturity Model β measure your security practices maturity | owasp.org/samm |
| NIST SP 800-160 | Systems Security Engineering β principles for secure system design | csrc.nist.gov |
| BSIMM | Building Security In Maturity Model β benchmark against industry | bsimm.com |
| Microsoft SDL | Security Development Lifecycle β Microsoft's battle-tested practices | microsoft.com/sdl |
| SAFECode | Fundamental Practices for Secure Software Development | safecode.org |
SAST / DAST / SCA / IAST Tools
Recommended Reading & References
π Essential Resources:
βββ OWASP Resources
β βββ OWASP Top 10 (2021)
β βββ OWASP ASVS v4.0 (Application Security Verification Standard)
β βββ OWASP Testing Guide v4.2
β βββ OWASP API Security Top 10
β βββ OWASP Cheat Sheet Series
β β βββ Authentication Cheat Sheet
β β βββ Authorization Cheat Sheet
β β βββ Session Management Cheat Sheet
β β βββ Input Validation Cheat Sheet
β β βββ Cryptographic Storage Cheat Sheet
β β βββ Error Handling Cheat Sheet
β β βββ Threat Modeling Cheat Sheet
β βββ OWASP Secure Coding Practices Quick Reference
β
βββ Books
β βββ "Threat Modeling: Designing for Security" β Adam Shostack
β βββ "The Web Application Hacker's Handbook" β Stuttard & Pinto
β βββ "Secure by Design" β Dan Bergh Johnsson et al.
β βββ "Building Secure and Reliable Systems" β Google SRE
β βββ "Security Engineering" β Ross Anderson
β
βββ Standards & Frameworks
β βββ NIST SP 800-53 (Security Controls)
β βββ NIST SP 800-160 (Systems Security Engineering)
β βββ ISO 27001/27002 (Information Security Management)
β βββ CWE/SANS Top 25 Most Dangerous Software Weaknesses
β βββ MITRE ATT&CK Framework
β
βββ Training & Practice
βββ OWASP WebGoat (Hands-on vulnerable app)
βββ OWASP Juice Shop (Modern vulnerable app)
βββ PortSwigger Web Security Academy (Free)
βββ HackTheBox / TryHackMe (Practice environments)
βββ Secure Code Warrior (Gamified secure coding)β The Ultimate Checklist
π Secure Design Checklist
Use this checklist during every design review. If you cannot check every box, you have identified areas that need attention.
Architecture & Design
- [ ] Threat model has been created and reviewed (STRIDE/PASTA)
- [ ] Data Flow Diagrams (DFDs) exist with trust boundaries marked
- [ ] Attack surface has been analyzed and minimized
- [ ] Defense-in-depth strategy is documented and implemented
- [ ] All components fail secure (not fail open)
- [ ] Zero trust principles applied (verify everything explicitly)
- [ ] Separation of duties enforced for critical operations
- [ ] No security-through-obscurity dependencies
- [ ] Third-party integrations risk-assessed
- [ ] Disaster recovery and business continuity planned
Authentication
- [ ] Multi-factor authentication (MFA) designed and implemented
- [ ] Password policy enforced (12+ chars, complexity, breach check)
- [ ] Brute-force protection (account lockout + rate limiting + CAPTCHA)
- [ ] Secure password recovery flow (no security questions)
- [ ] Credential stuffing defense (device fingerprinting, anomaly detection)
- [ ] Session management (short timeout, rotation, secure cookies)
- [ ] Account enumeration prevention (timing-safe, generic responses)
- [ ] Passwordless authentication option considered (WebAuthn/FIDO2)
Authorization
- [ ] RBAC/ABAC model designed and documented
- [ ] Principle of Least Privilege enforced across all roles
- [ ] Resource-level access control on every endpoint
- [ ] Server-side authorization enforcement (never client-only)
- [ ] Horizontal and vertical privilege escalation tested
- [ ] Authorization bypass explicitly tested in abuse cases
- [ ] Default deny policy (everything blocked unless explicitly allowed)
- [ ] Centralized policy engine (OPA, Casbin, or equivalent)
Data Protection
- [ ] Data classification completed (Public/Internal/Confidential/Restricted)
- [ ] Encryption at rest (AES-256-GCM or equivalent)
- [ ] Encryption in transit (TLS 1.3, strong cipher suites only)
- [ ] PII/sensitive data masking in logs and responses
- [ ] Secure key management (HSM, KMS, Vault β no hardcoded keys)
- [ ] Data retention and deletion policies defined and automated
- [ ] Backup encryption and testing
- [ ] Data minimization (collect only what you need)
Input / Output
- [ ] Server-side input validation on ALL endpoints (whitelist approach)
- [ ] Output encoding (context-sensitive: HTML, JS, URL, CSS, SQL)
- [ ] File upload restrictions (type, size, content verification, malware scan)
- [ ] Content Security Policy (CSP) headers configured
- [ ] All security headers set (HSTS, X-Frame-Options, X-Content-Type-Options)
- [ ] API request/response schema validation (OpenAPI/JSON Schema)
Business Logic
- [ ] Abuse cases written for every feature and tested
- [ ] Rate limiting on all critical endpoints (login, registration, API)
- [ ] Transaction integrity guaranteed (race conditions addressed)
- [ ] Idempotency designed into financial/state-changing operations
- [ ] Workflow bypass explicitly tested (can steps be skipped?)
- [ ] Negative testing performed (negative values, zero values, max values)
- [ ] Concurrent request handling tested
Monitoring & Incident Response
- [ ] Comprehensive audit logging designed (who, what, when, where)
- [ ] Logs are immutable and tamper-proof (append-only, signed)
- [ ] Security alerting mechanism configured (real-time)
- [ ] Incident response plan documented and rehearsed
- [ ] Log retention policy defined
- [ ] No sensitive data in logs (passwords, tokens, PII)
- [ ] Failed authentication and authorization attempts logged and alerted
π Conclusion
The Core Message
Key Takeaways
π΄ Insecure Design is not a code bug β it is a thinking bug.
It represents the absence of security considerations during the most critical phase of software development.
π‘ Perfect implementation can NEVER compensate for a flawed design.
You cannot patch a missing wall. You have to rebuild.
π’ Threat Modeling + Secure Design Patterns + Abuse Cases = Secure Systems.
These three practices, applied consistently, eliminate the vast majority of design-level vulnerabilities.
π΅ Security is not a feature β it is a property of the system.
It must be woven into the fabric of the architecture, not bolted on as an afterthought.
The Three Questions
Every time you design a new feature, endpoint, workflow, or system β ask yourself these three questions:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β 1. π€ "How will an ATTACKER abuse this feature?" β
β β Write abuse cases. Think adversarially. β
β β
β 2. π€ "What happens when this feature FAILS or ERRORS?" β
β β Design for failure. Fail secure, not open. β
β β
β 3. π€ "What is the MINIMUM permission needed for this?" β
β β Apply least privilege. Default deny. β
β β
β The moment you start asking these questions consistently, β
β you are thinking SECURE BY DESIGN. π― β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββThe Cost of Inaction
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BREACH COST BREAKDOWN β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Average cost of a data breach (2024): $4.88 MILLION β
β Average cost if secure design was applied: $0.15 MILLION β
β β
β Time to identify a breach (avg): 194 DAYS β
β Time to contain a breach (avg): 64 DAYS β
β β
β Breaches caused by design flaws: ~40% β
β Breaches preventable by threat modeling: ~60% β
β β
β Sources: IBM Cost of a Data Breach Report 2024, Ponemon Institute β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββFinal Words
Security is not something you add at the end. It is not a sprint task you defer. It is not a checkbox on a compliance form. It is the foundation upon which reliable, trustworthy software is built.
Insecure Design (A04:2021) exists in the OWASP Top 10 as a reminder that no amount of firewalls, WAFs, encryption, or monitoring can save a system that was fundamentally designed to be insecure.
Start with the design. Start with the threat model. Start with the abuse cases.
Everything else follows.
<div align="center">
π Further Reading
| Resource | URL |
|---|---|
| OWASP Top 10 β 2021 | https://owasp.org/Top10/ |
| OWASP ASVS v4.0 | https://owasp.org/www-project-application-security-verification-standard/ |
| OWASP Threat Dragon | https://owasp.org/www-project-threat-dragon/ |
| OWASP Cheat Sheet Series | https://cheatsheetseries.owasp.org/ |
| NIST SSDF | https://csrc.nist.gov/Projects/ssdf |
| MITRE CWE | https://cwe.mitre.org/ |
| HaveIBeenPwned | https://haveibeenpwned.com/ |
Category: Application Security Β· Secure Architecture Β· OWASP Tags: #OWASP #InsecureDesign #ThreatModeling #SecureSDLC #AppSec #SecurityArchitecture #A04
"Security is not a product, but a process." β Bruce Schneier
"The only truly secure system is one that is powered off, cast in a block of concrete, and sealed in a lead-lined room with armed guards β and even then I have my doubts." β Gene Spafford
"If you think technology can solve your security problems, then you don't understand the problems and you don't understand the technology." β Bruce Schneier
</div>