đŸ›Ąïž Application Security CheatSheet

Business Logic & Race Conditions Deep Dive

Business Logic vulnerabilities happen when the application follows the developer’s code correctly but still produces an outcome the business would never want (free items, negative totals, bypassed approvals, wrong account credited, incorrect limits).

Race Conditions happen when the outcome depends on timing—two requests overlap and the system makes a decision using stale assumptions (double refunds, oversold inventory, repeated bonus credits, bypassed rate/limit checks).

Key idea: These aren’t “input injection” bugs. They’re system behavior bugs: missing invariants, missing state checks, and missing atomicity.

What I see in reviews: business logic bugs thrive in “works as designed” conversations. The exploit is usually just using the feature more cleverly than expected.

Deep reason these exist (what goes wrong in real systems)

These issues often evade scanners because the payload isn’t “special”. The exploit is sequence, state, and timing.

First principles mental model

Think like a experienced engineer: define invariants and enforce them at the right layer.

  1. Invariants: rules that must always hold (e.g., “balance never negative”, “coupon applied at most once”, “order total computed server-side”).
  2. State machine: allowed transitions (Created → Paid → Shipped → Completed). Disallow skips and repeats.
  3. Authority: server is the source of truth for price, permissions, and state—not the client.
  4. Atomicity: checks and updates must be one unit (transaction) when they must not be interleaved.
  5. Idempotency: repeating the same request should not cause repeated effects (especially payments/refunds/credits).
experienced rule: For money, inventory, limits, approvals, and rewards: enforce invariants in the database/service layer, not only in UI.

Vulnerable vs secure code patterns (Node.js)

Vulnerable pattern #1: trusting client-calculated totals

// Express (concept example)
app.post("/checkout", async (req, res) => {
  const { items, total, currency } = req.body;

  // ❌ Business logic bug: trusting client "total"
  await db.orders.insert({
    userId: req.user.id,
    items,
    total,
    currency,
    status: "PENDING_PAYMENT",
  });

  res.json({ ok: true });
});

Fixed pattern #1: compute totals server-side + validate invariants

// Concept: compute from trusted catalog/pricing rules
app.post("/checkout", async (req, res) => {
  const items = Array.isArray(req.body.items) ? req.body.items : [];
  if (items.length === 0) return res.status(400).json({ ok: false, error: "No items" });

  // ✅ Pull prices from trusted source (DB/service), not from client
  const productIds = items.map(i => String(i.productId || ""));
  const products = await db.products.findMany({ id: { $in: productIds } });

  // Build server-side line items and totals
  let subtotal = 0;
  const normalized = [];
  for (const it of items) {
    const pid = String(it.productId || "");
    const qty = Number(it.qty || 0);
    if (!Number.isFinite(qty) || qty <= 0 || qty > 100) return res.status(400).json({ ok: false, error: "Bad qty" });

    const p = products.find(x => x.id === pid);
    if (!p) return res.status(400).json({ ok: false, error: "Unknown product" });

    const line = p.priceCents * qty;
    subtotal += line;
    normalized.push({ productId: pid, qty, unitPriceCents: p.priceCents, lineTotalCents: line });
  }

  // ✅ Apply discounts/coupons server-side only (if any), then enforce invariants
  const totalCents = subtotal; // simplified
  if (totalCents < 0) return res.status(400).json({ ok: false, error: "Invalid total" });

  const order = await db.orders.insert({
    userId: req.user.id,
    items: normalized,
    subtotalCents: subtotal,
    totalCents,
    status: "PENDING_PAYMENT",
  });

  res.json({ ok: true, orderId: order.id });
});

Vulnerable pattern #2: race on “check then update” (inventory)

// ❌ Non-atomic: two requests can both pass the check before either updates
app.post("/reserve", async (req, res) => {
  const sku = String(req.body.sku || "");
  const qty = Number(req.body.qty || 0);

  const stock = await db.inventory.findOne({ sku });
  if (!stock || stock.available < qty) return res.status(409).json({ ok: false, error: "Out of stock" });

  await db.inventory.update({ sku }, { available: stock.available - qty });
  res.json({ ok: true });
});

Fixed pattern #2: atomic update with invariant enforced in DB

// ✅ Atomic "update if invariant holds" (pattern depends on DB)
// Concept example: update only when available >= qty, then check rows affected
app.post("/reserve", async (req, res) => {
  const sku = String(req.body.sku || "");
  const qty = Number(req.body.qty || 0);
  if (!Number.isFinite(qty) || qty <= 0) return res.status(400).json({ ok: false, error: "Bad qty" });

  const result = await db.inventory.updateWhere(
    { sku, available: { $gte: qty } },
    { $inc: { available: -qty } }
  );

  if (result.modifiedCount !== 1) return res.status(409).json({ ok: false, error: "Out of stock" });
  res.json({ ok: true });
});
experienced takeaway: Race fixes live where truth lives: DB constraints, atomic updates, transactions, and idempotency keys—not only “better if-statements”.

Common “gotchas” that still create logic bugs (even in modern stacks)

Where these vulnerabilities appear (high-signal areas)

Business logic patterns to recognize (the “why” behind each)

Detection workflow (experienced-style, systematic)

  1. Model the workflow: write the intended state machine and invariants (money, inventory, approvals, limits).
  2. Identify trust boundaries: which fields must be server-derived vs client-supplied.
  3. Map endpoints to stages: what endpoints move state forward; what endpoints modify money/credits/stock.
  4. Look for gaps: missing state checks, missing ownership checks, missing idempotency, non-atomic checks.
  5. Consider concurrency: any “check then update” pattern; any balance/stock mutation without transaction/atomic update.
  6. Verify with safe tests: use controlled test accounts and logs to confirm behavior (no harmful probing).
Interview framing: “I don’t hunt for payloads; I hunt for broken invariants and non-atomic state transitions.”

How to prove issues without giving “weaponized” steps

Goal: demonstrate the broken invariant with minimal risk and clear evidence.
Avoid sharing step-by-step racing methods or exploit playbooks. In interviews, emphasize safe reproduction and root-cause validation.

How exploitation progresses (attacker mindset)

Conceptual only. Attackers usually treat the application as a state machine and look for ways to: (1) feed the server “derived” values it shouldn’t trust, (2) skip required transitions, or (3) make two operations overlap.

Phase 1: Learn the workflow and its invariants

Phase 2: Look for trust and state gaps

Phase 3: Probe timing windows (race conditions)

Phase 4: Chain into higher impact

Interview takeaway: The attacker’s advantage is patience and sequencing. Your defense is invariants + atomicity + idempotency + auditability.

What makes a finding “high confidence” vs “maybe”

ConfidenceWhat you observedWhat you can claim
Low Spec mismatch suspected but business rule unclear or not measurable “Potential logic gap; needs product confirmation of intended invariant/state transitions.”
Medium Observable inconsistency, but impact depends on constraints (fraud checks, reconciliation, manual review) “Likely vulnerability; recommend enforcing invariant at source-of-truth layer and adding controls.”
High Repeatable invariant violation with clear evidence (double credit, negative stock/balance, repeated execution) “Confirmed business logic/race condition with clear root cause and actionable remediation.”

Fixes that actually hold in production

1) Enforce invariants at the source of truth

2) Make state transitions explicit

3) Add idempotency for side-effect actions

4) Design for concurrency

5) Defense-in-depth

Practical priority order: invariants in DB/service → explicit state machine → idempotency keys → atomic updates/transactions → audit + monitoring.

Regression prevention (how to prevent regressions)

Interview Questions & Answers (Easy → Hard)

How to answer: Start with “invariants + state machine”, then talk about atomicity, idempotency, and where enforcement belongs.

Easy

  1. What is a business logic vulnerability?
    A: Plain: the app does what it was coded to do, but the result breaks the business rules. Deep: missing invariants/state checks, trusting client-derived values, or inconsistent enforcement across services.
  2. What is a race condition?
    A: Plain: timing changes the result. Deep: non-atomic “check then update” lets overlapping requests violate invariants (double execute, oversell, bypass limits).
  3. Why are these hard to find with scanners?
    A: Plain: there’s no “special input”. Deep: the exploit is sequence and timing; you need workflow modeling and state reasoning.
  4. Give examples of invariants.
    A: Plain: rules that must always be true. Deep: totals computed server-side, coupon once per order, balance never negative, stock never below zero, approvals required before payout.
  5. Where should pricing be computed?
    A: Plain: on the server. Deep: server is the source of truth; compute from trusted catalog and discount rules and validate results before charging/shipping.
  6. What’s idempotency?
    A: Plain: repeating a request doesn’t repeat the effect. Deep: required for payments/refunds/credits because retries and timeouts happen; enforce with idempotency keys and DB uniqueness.

Medium

  1. Scenario: A coupon can be applied multiple times. How do you fix it?
    A: Plain: enforce “once” on the server. Deep: store coupon usage with a unique constraint (user×coupon or order×coupon), compute discounts server-side, and enforce state rules so it can’t be re-applied.
  2. Scenario: Users can change item price in the request. What do you do?
    A: Plain: ignore client price. Deep: fetch price from trusted DB/service, recompute totals server-side, sign critical data only if needed, and log anomalies.
  3. Scenario: “Refund” sometimes executes twice during timeouts. Root cause?
    A: Plain: retries without protection. Deep: missing idempotency and non-atomic state transitions; fix with idempotency keys, unique constraints, and transactional status changes.
  4. Follow-up: How do you explain atomicity to juniors?
    A: Plain: “check and update must be one action.” Deep: otherwise two requests can pass the check; use transactions or conditional updates that enforce invariants.
  5. Scenario: Inventory goes negative under load. Where do you enforce it?
    A: Plain: at the database/service layer. Deep: use atomic decrement with a condition (available ≄ qty) or transaction/lock; don’t rely on app-level read-then-write.
  6. Follow-up: What’s the difference between optimistic and pessimistic locking?
    A: Plain: optimistic assumes few conflicts; pessimistic prevents conflicts. Deep: optimistic uses version checks and retries; pessimistic uses locks/transactions—choose based on contention and correctness needs.
  7. Scenario: Rate limiting is per-node and can be bypassed via multiple nodes. How do you fix?
    A: Plain: centralize it. Deep: shared store (Redis) with atomic counters, consistent keys, and server-side enforcement tied to identity and action type.

Hard

  1. Scenario: A multi-step payout flow can be “skipped” to force payout early. What do you propose?
    A: Plain: enforce the state machine. Deep: explicit transitions with validation, deny-by-default transitions, transactional state changes, and audit events for every step.
  2. Scenario: Distributed services update the same balance. How do you prevent inconsistencies?
    A: Plain: one owner for the balance. Deep: single service/source of truth, append-only ledger/events, transactional updates, and reconciliation jobs; avoid multiple writers without coordination.
  3. Follow-up: When do you use DB constraints vs app checks?
    A: Plain: both, but constraints for critical invariants. Deep: app checks improve UX; DB constraints guarantee correctness under concurrency and unexpected code paths.
  4. Scenario: You must allow concurrent checkout at high scale. How do you balance correctness and performance?
    A: Plain: use atomic operations and minimize lock scope. Deep: conditional updates, short transactions, partitioning by SKU, and eventual reservation patterns while keeping “no oversell” invariant enforced.
  5. Follow-up: What metrics indicate a race condition in production?
    A: Plain: duplicates and inconsistent states. Deep: duplicate ledger entries, repeated refunds, negative stock, spikes in retries/timeouts, and mismatch between audit events and final state.
  6. Scenario: A “first-time bonus” is claimed twice. What’s the strongest fix?
    A: Plain: make it “claim once” at DB level. Deep: unique constraint on (userId, bonusType), transactional check+insert, idempotency keys, and audit trail to detect anomalies.
  7. Follow-up: How do you report these issues clearly?
    A: Plain: explain the broken rule and outcome. Deep: document invariant/state machine, show before/after state evidence, quantify impact, and propose durable fixes (atomicity/idempotency/constraints).
Safety note: for understanding +