Juicy Shop
Insecure Design
Some flaws can't be patched because the design itself is unsafe. Learn the difference between a bug and a design flaw, how threat modelling surfaces risks early, and why business-logic abuse needs design-level defences.
A flaw you can't patch
Security misconfiguration and injection are implementation bugs — the design was fine, the code was wrong. Insecure design is different: the feature works exactly as written, but the concept is unsafe. A password-reset flow that emails the new password, an unlimited-retry coupon redemption, a 'trust the client-supplied price' checkout — no amount of careful coding fixes these, because the design never accounted for an adversary. You cannot patch your way out of a missing security requirement.
Threat modelling shifts security left
The cheapest place to find a design flaw is on a whiteboard, before any code exists. Threat modelling asks four questions about a feature: what are we building, what can go wrong, what are we going to do about it, and did we do a good enough job. Structured methods like STRIDE (Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege) prompt you to consider each abuse category against every trust boundary in your data-flow diagram.
Business-logic abuse
Many design flaws are abuse of legitimate functionality, not technical exploits. An attacker doesn't break your code — they use it in a way you didn't intend: redeeming the same gift card a thousand times in parallel, ordering a negative quantity to get a credit, skipping a checkout step the server assumes always happens. These pass every input validator because the inputs are individually valid; the flaw is in the workflow's assumptions.
Designing for misuse
Bake security into the design: write explicit abuse cases alongside user stories, enforce business rules and invariants on the server (never trust client-side checks), add rate limits and resource quotas to costly or sensitive flows, and require re-authentication for high-value actions. Use secure design patterns and reference architectures rather than inventing flows from scratch. The goal is a design where the safe path is the only path.
Insecure vs secure
app.post("/checkout", (req, res) => {
// Trusts the price the client sent
const { items, totalPrice } = req.body;
charge(req.user, totalPrice);
createOrder(req.user, items);
});The server trusts a client-supplied total, so an attacker simply posts totalPrice: 0. No input is 'invalid' — the design itself is the flaw.
app.post("/checkout", async (req, res) => {
const { items } = req.body;
// Server is the source of truth for price
const total = await priceFromCatalog(items);
await enforceQuantityLimits(items);
await charge(req.user, total);
await createOrder(req.user, items);
});The server derives the price and enforces invariants (limits, ownership) itself, so the workflow cannot be abused regardless of what the client sends.
Key takeaways
- Insecure design is an unsafe concept, not a coding bug — you can't patch it.
- Threat-model features early; STRIDE prompts abuse cases per trust boundary.
- Business-logic abuse uses valid inputs in unintended workflows.
- Enforce invariants and pricing/limits server-side; never trust the client.
- Write abuse cases beside user stories and prefer proven design patterns.
Knowledge check
Practice
Apply this lesson hands-on in the matching find-it / fix-it challenge.
Open challenge →