Skip to main content

Insecure Design

Updated 5 min readMarkdown
Insecure Design

Insecure Design is about flaws baked into the architecture/logic before any code was even written — the system does exactly what it was built to do, and that's the problem.

The core distinction

  • Implementation flaw: "the login check was supposed to verify the password, but the code has a bug that skips it"

  • Design flaw: "the system was never designed to limit how many password attempts someone gets" — nothing's broken, it just was never designed with that protection in mind

Common examples

  • No rate limiting on sensitive actions — password reset, login, OTP verification — allowing brute force simply because nobody designed a limit

  • Trusting client-side logic for business rules — e.g., a checkout flow where the price is calculated in JavaScript and just trusted by the server, rather than the server recalculating it independently

  • Overly trusting workflows — a multi-step process (like a purchase flow) where skipping a step (going straight to "confirm order" without "add payment") isn't prevented, because the design assumed users would always go in order

  • Insufficient anti-automation — signup forms, coupon codes, etc. with no CAPTCHA/rate limiting, designed assuming only "normal" human usage.

Lab Scenario: Nimbus Electronics Shopping Cart

The feature, as designed:

POST /cart/add
{ "productId": 42, "quantity": 5 }

Server logic:

function addToCart(productId, quantity) {
  const product = getProduct(productId);
  const lineTotal = product.price * quantity;
  cart.addItem(product, quantity, lineTotal);
  cart.total += lineTotal;
}

What the designer assumed, unstated: quantity will always be a positive whole number, because why would anyone add "-5 shirts" to a cart? Nobody writes a requirement document that says "quantity must be positive" because it feels too obvious to even need saying — and that's exactly the trap.

The exploit, step by step:

You have two items in your cart:

  • 1x Laptop — $1000

  • You add: { "productId": 43, "quantity": -10 } for a $50 accessory

The math runs exactly as written, faithfully, with no validation stopping it:

lineTotal = 50 * -10 = -500
cart.total = 1000 + (-500) = 500

You now owe $500 for a $1000 laptop, because the system happily subtracted money from your total — the code isn't buggy, price * quantity is completely correct arithmetic. The flaw is that nobody designed a rule saying quantities can't be negative, because it felt too obvious to write down.

Depending on how checkout finalizes this, you might even be able to push the total to exactly $0, or negative — some real-world versions of this exact bug have resulted in stores owing customers money at checkout, because nobody capped the total at a minimum of $0 either. Same root assumption failure, compounding.

Why this specific type is so common and so easy to miss

they validate for type (is quantity a number?) but not for real-world sense (is this a number that makes sense in context?). -10 passes every technical check — it's a valid integer, it fits in the database field, the multiplication doesn't error. Nothing about it looks "broken" to an automated test or a type-checker. It only looks wrong if you're thinking like a human about what a shopping cart means, not just what the code technically allows.

Scenario 2: inconsistent handling of exceptional input

The idea: a system might handle normal input one way, and unusual/edge-case input in a subtly different way — and that difference in behavior itself becomes exploitable, even if neither path is individually "broken."

Registration with Duplicate Prevention This lab has a registration form and an account update / email change feature. The intended business rule: only one account per email address.

Normal behavior, as designed: Register with user@example.com → succeeds, account created Try to register again with the exact same user@example.com → correctly rejected: "email already in use" The exceptional input that breaks the assumption: The lab's actual vulnerable feature is the "update email" flow on your account. The intended rule is the same: you shouldn't be able to change your email to one that's already registered to someone else. Here's the twist that makes it "inconsistent handling": the validation check and the actual email-saving logic don't treat the input identically. Specifically, the duplicate-check might be case-sensitive or exact-match, while the actual update either doesn't re-validate, or handles a slightly malformed/edge-case version of the email differently than the check did. The real, official solve for this specific lab: you can update your email to include a second email address appended after a special separator character — many backend systems (in real implementations, things like extra @ symbols, or padding an email with attacker-chosen characters after a null byte or separator) get parsed inconsistently between validation (which checks the whole messy string and doesn't find a match, so it "passes") and the actual account update / notification logic (which might only read up to the first valid email portion, or normalize the input differently) — resulting in you effectively taking over another account because the validation layer and the execution layer disagreed about what the "real" input actually was.

About the author

No comments yet. Be the first to comment.