Until now we've fixed implementation flaws: a concatenated query, a forgotten authorization check, a dangerous innerHTML. But there's a class of vulnerabilities that no code fix eliminates, because the problem isn't in how the code was written but in what was decided to be built. That's A04:2021 – Insecure Design, one of the two new categories in the 2021 Top Ten (along with A10 SSRF).
The core idea: you can implement an insecure design flawlessly —clean code, no SQLi, no XSS— and still be vulnerable, because security controls were missing from the design itself. In BazarNube we'll see it with the discount coupon and checkout flow: a design that never considered business limits lets a customer stack discounts until they pay zero euros. There's no code bug at all; the flaw is that nobody thought about abuse when designing the flow.
This lesson introduces threat modeling as a tool to discover these flaws early. We present it briefly here; its full treatment is in module 7 (lesson 07-02).
Legal and ethical notice: the abuse scenarios are illustrative and use fake data. Practice only on your own systems or with explicit authorization.
Contents
- Insecure design vs. insecure implementation
- The BazarNube coupon case
- Security requirements and abuse cases
- Trust boundaries and secure design patterns
- Introduction to threat modeling
- Common mistakes, exercises and solutions
- Insecure design vs. insecure implementation
| Aspect | Insecure implementation | Insecure design |
|---|---|---|
| Origin | A control that existed was implemented badly | The control was missing from the design |
| Example | Concatenated SQL query (A03) | No attempt or business limit was defined |
| Detected with | Code review, SAST/DAST | Threat modeling, requirements review |
| Fixed with | Patching the code | Redesigning the flow / adding controls |
A hint to tell them apart: if you fix the flaw by changing how a function is written, it was implementation; if you need to add a control or business rule that didn't exist, it was design. Many real incidents combine both, but A04 forces us to look before the code: does the flow, as conceived, hold up against a malicious user?
- The BazarNube coupon case
The product team asked for discount coupons to attract customers. Marc and Lucía implemented the flow exactly as specified, and the code is correct: it validates the coupon, checks that it exists, applies the percentage. The checkout endpoint:
// checkout.js — CORRECT code but INSECURE design
router.post('/api/checkout', requireLogin, async (req, res) => {
const cart = await getCart(req.session.userId);
let total = cartTotal(cart);
for (const code of req.body.coupons) { // accepts a LIST of coupons
const c = await getCoupon(code);
if (c && c.active) total -= total * (c.percent / 100); // all are applied
}
const order = await createOrder(req.session.userId, cart, total);
res.json({ orderId: order.id, total });
});There's no SQLi, no IDOR, no XSS. The code does exactly what was designed. The problem is what was not designed:
- The number of coupons per order wasn't limited (the flow accepts a list).
- It wasn't checked whether a coupon is stackable with others.
- No maximum discount or minimum amount to pay was validated.
- The number of uses per customer wasn't limited.
How it's exploited (logic abuse)
A customer discovers three valid coupons for 40%, 35% and 30%. They send them all in the same checkout:
Applied in cascade, the total drops to almost zero. Worse: since there's no usage limit, they automate orders and drain the stock at a symbolic cost. No code-scanning tool detects it, because there's no insecure pattern: it's a business decision that was missing.
Fix: add the design controls
The solution isn't to "patch a line," it's to incorporate the missing business security requirements:
// checkout.js — SECURE design: explicit business rules
router.post('/api/checkout', requireLogin, async (req, res) => {
const cart = await getCart(req.session.userId);
const subtotal = cartTotal(cart);
// Rule 1: a single coupon per order (not stackable by default)
const code = req.body.coupon;
let discount = 0;
if (code) {
const c = await getCoupon(code);
if (!c || !c.active) return res.status(400).json({ error: 'Invalid coupon' });
// Rule 2: usage limit per customer
if (await couponUsedBy(code, req.session.userId) >= c.maxPerUser)
return res.status(400).json({ error: 'Coupon already used' });
// Rule 3: minimum purchase amount
if (subtotal < c.minPurchase)
return res.status(400).json({ error: 'Minimum not reached' });
discount = subtotal * (c.percent / 100);
}
// Rule 4: global maximum discount and minimum amount to pay
discount = Math.min(discount, subtotal * 0.5);
const total = Math.max(subtotal - discount, 0.01);
const order = await createOrder(req.session.userId, cart, total, code);
res.json({ orderId: order.id, total });
});Notice that the fix consists of business security rules, not coding techniques: one coupon per order, usage limit, minimum purchase, discount cap, and a minimum amount to pay. These rules should have surfaced during the design phase, not been discovered after the abuse.
- Security requirements and abuse cases
The root of the flaw was working only with use cases ("the customer applies a coupon and pays less") and forgetting the abuse cases ("the customer combines coupons to pay zero"). Designing with security means writing both.
| Use case (legitimate user) | Abuse case (attacker) | Derived security requirement |
|---|---|---|
| Applies a welcome coupon | Combines several coupons | At most one coupon per order |
| Uses their coupon once | Reuses the coupon in a loop | Usage limit per customer |
| Buys at a discount | Forces total = 0 | Minimum amount to pay and discount cap |
| Registers to receive a coupon | Creates accounts en masse to farm coupons | Email verification / limit per identity |
Each abuse case gives rise to a security requirement that must be part of the specification, not a later add-on. This is the heart of A04: security as a design requirement.
- Trust boundaries and secure design patterns
A trust boundary is any frontier where data crosses from a less trusted zone to a more trusted one (from browser to server, from server to the payments module). At every boundary you must validate and re-decide. The frequent design mistake is to assume that "if it comes from the previous step, it's trustworthy."
flowchart LR A[React client] -->|trust boundary| B[Express API] B -->|trust boundary| C[Java payments module] C -->|trust boundary| D[External PSP]
Secure design patterns that BazarNube adopts:
- Server-side validation of business rules, not only on the client (the client is a suggestion, the server is the authority).
- Limits and quotas by design: rate limiting, maximums per operation, minimum amounts.
- Explicit states and transitions: an order can't go from "created" to "shipped" without passing through "paid." Designing the state machine prevents abusive jumps.
- Fail-safe / deny by default (we saw it in A01): when in doubt, deny.
- Reuse proven, secure components instead of reinventing them (payment, authentication libraries).
- Introduction to threat modeling
Threat modeling is the practice that would have prevented the coupon flaw: analyzing the design before building, asking what can go wrong. In brief, it answers four questions (Shostack's framework):
- What are we building? (flow diagram, actors, trust boundaries)
- What can go wrong? (threats; a common method is STRIDE)
- What are we going to do about it? (controls/mitigations)
- Did we do a good job? (validation)
Applied to the coupons, the question "what can go wrong?" would have immediately revealed "a customer combines coupons" and "uses them in a loop," generating the missing requirements.
We don't develop the full method here (STRIDE, data-flow diagrams, prioritization): that belongs to module 7, lesson 07-02 (Threat Modeling). For now, hold on to the practical idea: build a threat-modeling session into the design of every sensitive flow (payments, coupons, registration, permissions), and turn each threat into a verifiable security requirement.
Common Mistakes and Tips
- Confusing "bug-free" with "secure." A flow can have not a single code bug and still be abusable by design.
- Designing only happy-path use cases. Write the abuse cases too; that's where the requirements come from.
- Validating business rules only on the front end. The client is manipulable; the authority is the server.
- Leaving security for "later." Redesigning a flow in production is very expensive; threat modeling early is cheap.
- Not defining limits or quotas. Every economic or resource flow needs explicit caps.
- Tip: for each new sensitive feature, spend 30 minutes asking "how would I abuse this?" before implementing it.
Exercises
Exercise 1. BazarNube designs a "loyalty points" system: the customer earns points per purchase and redeems them for balance. List three abuse cases and their derived security requirement.
Exercise 2. Which of these flaws is a design flaw and which is an implementation flaw? Justify. a) The coupon endpoint concatenates the code into a SQL query. b) The system lets you return a product and keep it, because it doesn't verify that it was received at the warehouse.
Exercise 3. The team proposes "fixing" the coupon abuse by validating on the front end that only one coupon is sent. Does that solve the problem? Why?
Solutions
Solution 1. Examples: (a) Abuse: generate purchases and returns to farm points → Requirement: only credit points after a completed and non-refunded order. (b) Abuse: redeem more points than you have via a race condition → Requirement: atomic, transactional deduction of the points balance. (c) Abuse: transfer points between your own mass-created accounts → Requirement: limit per verified identity / non-transferable.
Solution 2. a) Implementation: the control (parameterizing) exists as a practice; a query was implemented badly. b) Design: a rule/state is missing from the returns flow (verify receipt before refunding); it's not a coding bug but an absent control.
Solution 3. It doesn't solve it. Front-end validation is only UX: an attacker calls POST /api/checkout directly with several coupons, bypassing the browser. Business controls must be enforced on the server (as in section 2). The front end can guide, but it cannot protect.
Conclusion
A04 broadens the view: security begins in the design, not in the code. Insecure-design flaws aren't caught by a scanner; they're prevented by writing abuse cases, deriving security requirements, respecting trust boundaries, and applying threat modeling early. In BazarNube, redesigning the coupon flow with explicit business limits closed a hole that no line-level fix would have plugged.
Backlog entry — A04: redesigned the coupon checkout with business rules (one coupon per order, usage limit, minimum purchase, discount cap, minimum amount to pay); adopted a threat-modeling session for economic flows; pending application of the full method in M7.
From design, we now move to how what's built is deployed and configured. Even if the code and the design are correct, careless configuration opens the door. The next lesson is A05:2021 – Security Misconfiguration, with BazarNube's Express and Docker configuration.
OWASP Course: Guidelines and Standards for Web Application Security
Module 1: Introduction to OWASP
Module 2: Main OWASP Projects
- OWASP Top Ten
- OWASP ASVS (Application Security Verification Standard)
- OWASP SAMM (Software Assurance Maturity Model)
- OWASP ZAP (Zed Attack Proxy)
- Other Key Projects: WSTG, Cheat Sheets and Dependency-Check
Module 3: OWASP Top Ten 2021 in Depth
- A01:2021 – Broken Access Control
- A02:2021 – Cryptographic Failures and Sensitive Data Exposure
- A03:2021 – Injection
- Cross-Site Scripting (XSS) in Depth
- A04:2021 – Insecure Design
- A05:2021 – Security Misconfiguration
- XML External Entities (XXE)
- A06:2021 – Vulnerable and Outdated Components
- A07:2021 – Identification and Authentication Failures
- A08:2021 – Software and Data Integrity Failures (Insecure Deserialization)
- A09:2021 – Security Logging and Monitoring Failures
- A10:2021 – Server-Side Request Forgery (SSRF)
Module 4: OWASP ASVS (Application Security Verification Standard)
Module 5: OWASP SAMM (Software Assurance Maturity Model)
Module 6: OWASP ZAP (Zed Attack Proxy)
- Introduction to ZAP
- Installation and Configuration
- Vulnerability Scanning
- Automating Security Testing
Module 7: Best Practices and Recommendations
- Secure Software Development Life Cycle (SDLC)
- Threat Modeling
- Integrating Security into DevOps (DevSecOps)
- Security Training and Awareness
- Additional Tools and Resources
Module 8: Practical Exercises and Case Studies
- Exercise 1: Identifying Vulnerabilities
- Exercise 2: Implementing Security Controls
- Case Study 1: Analyzing a Security Incident
- Case Study 2: Improving the Security of a Web Application
