Business Logic

Logic FlawsWorkflow AbusePricing and Promotions

Business Logic at a glance

What it is: Bugs in how the app enforces rules, prices, and workflows. Not missing crypto or auth, but mistakes in the business decisions the code makes.
Why it happens: Lets attackers get goods or credits for free
How to fix: Do not trust client computed values (price, totals, discounts); Enforce state machines for workflows and validate transitions; Apply server side limits, floors, and exclusivity rules

Overview

Business logic flaws arise when code implements the wrong rule, not when a security control is technically missing. Classic examples include trusting client prices, allowing negative quantities, stacking incompatible discounts, skipping workflow preconditions, or failing to make single use actions idempotent.

The fixes live in your application code, close to where the rule is implemented, and usually require explicit allow lists, state machines, and server side calculations.

sequenceDiagram participant Browser participant App as App Server participant PSP as Payment Provider Browser->>App: POST /checkout { items, total: 1 } App->>PSP: charge amount=1 PSP-->>App: ok App-->>Browser: 200 OK charged=1
A potential flow for a Business Logic exploit

Where it occurs

They show up in carts, checkout, refunds, coupon engines, and any workflow with preconditions such as payment before shipment. Fast feature work, incomplete tests, and copy pasted handlers are common root causes.

Impact

Real money loss from undercharges and over refunds, inventory leakage, and abusable credits. Attackers often automate these bugs at scale which leads to fraud spikes and customer support load.

Prevention

Calculate monetary values on the server from a trusted catalog. Validate types and bounds for quantities. Make single use operations transactional and idempotent. Enforce state transitions in one place, not per handler. Add property based tests and negative test cases that try invalid transitions, negative or fractional quantities, and coupon combinations.

Examples

Switch tabs to view language/framework variants.

Express, checkout trusts price from the client

Order total is taken from req.body.total and sent to the PSP as-is.

Vulnerable
JavaScript • Express — Bad
const express = require('express');
const app = express();
app.use(express.json());

app.post('/checkout', async (req, res) => {
  const { items, total } = req.body; // BUG: total from client
  // pretend to charge PSP with total
  const chargeOk = true;
  if (!chargeOk) return res.status(402).end();
  return res.json({ ok: true, charged: total });
});
  • Line 6: Uses client-provided total for payment

Client controlled totals let attackers pay less or create negative charges by manipulating the request.

Secure
JavaScript • Express — Good
const express = require('express');
const app = express();
app.use(express.json());

const CATALOG = { PRO: 19900 };
function computeTotal(items){
  let sum = 0;
  for (const it of items){
    if (!CATALOG[it.sku]) throw new Error('unknown sku');
    const qty = Number(it.qty);
    if (!Number.isInteger(qty) || qty < 1 || qty > 10) throw new Error('bad qty');
    sum += CATALOG[it.sku] * qty;
  }
  return sum;
}

app.post('/checkout', async (req, res) => {
  try {
    const amount = computeTotal(req.body.items);
    const chargeOk = true; // PSP charge with verified amount
    if (!chargeOk) return res.status(402).end();
    return res.json({ ok: true, charged: amount });
  } catch (e) {
    return res.status(400).json({ error: 'bad cart' });
  }
});
  • Line 5: Price list on server
  • Line 6: Server computes total and validates quantities

Server computes final amount from a trusted catalog and enforces quantity limits and integer checks.

Engineer Checklist

  • Compute price and totals on the server from a trusted catalog

  • Validate quantity as positive integers within bounds

  • Enforce exclusive or compatible coupon sets with a zero floor

  • Make single use actions transactional and idempotent

  • Guard workflow transitions with an allow listed state machine

End-to-End Example

An Express shop accepts totals from the client and uses them for charging. Attackers modify the request and pay a fraction of the real cost.

Vulnerable
JAVASCRIPT
// Node.js/Express - Vulnerable business logic

// VULNERABLE: Trusts client-provided total
app.post('/checkout', async (req, res) => {
  const { items, total } = req.body;
  
  // VULNERABLE: Accepts total from client without verification!
  // Attacker sends: {items: [{id:1,price:100}], total: 1}
  // Gets $100 item for $1
  
  const chargeResult = await stripe.charges.create({
    amount: total * 100,  // Uses client's total!
    currency: 'usd',
    customer: req.user.stripeId
  });
  
  res.json({ success: true, charged: total });
});

// VULNERABLE: No quantity limits
app.post('/add-to-cart', async (req, res) => {
  const { productId, quantity } = req.body;
  
  // VULNERABLE: Accepts any quantity, including negative!
  // Attacker sends quantity: -10 to get refund
  // Or quantity: 999999 to break inventory
  
  await db.cart.insert({
    userId: req.user.id,
    productId,
    quantity  // No validation!
  });
  
  res.json({ success: true });
});

// VULNERABLE: Coupon reuse
app.post('/apply-coupon', async (req, res) => {
  const { code } = req.body;
  
  const coupon = await db.coupons.findOne({ code });
  
  if (!coupon) {
    return res.status(404).json({ error: 'Invalid coupon' });
  }
  
  // VULNERABLE: Doesn't check if coupon already used!
  // Attacker applies same 50% off coupon multiple times
  
  req.session.discount = coupon.percentage;
  
  res.json({ discount: coupon.percentage });
});

// VULNERABLE: No rate limiting on expensive operations
app.post('/generate-report', async (req, res) => {
  const { startDate, endDate } = req.body;
  
  // VULNERABLE: No limits on date range or request frequency
  // Attacker requests 10-year reports 100 times/sec
  // Causes CPU/memory exhaustion
  
  const data = await db.transactions.find({
    date: { $gte: startDate, $lte: endDate }
  });
  
  const report = generateHugeReport(data);  // CPU intensive
  
  res.json({ report });
});

// VULNERABLE: No stock checking
app.post('/purchase', async (req, res) => {
  const { productId, quantity } = req.body;
  
  const product = await db.products.findById(productId);
  
  // VULNERABLE: Doesn't verify stock before purchase!
  // Multiple users can buy last item simultaneously
  
  await db.orders.create({
    userId: req.user.id,
    productId,
    quantity,
    price: product.price * quantity
  });
  
  // Stock decremented after order created - race condition!
  await db.products.update(
    { _id: productId },
    { $inc: { stock: -quantity } }
  );
  
  res.json({ success: true });
});

// VULNERABLE: State machine bypass
app.post('/orders/:id/refund', async (req, res) => {
  const order = await db.orders.findById(req.params.id);
  
  if (order.userId !== req.user.id) {
    return res.status(403).json({ error: 'Not your order' });
  }
  
  // VULNERABLE: Doesn't check order status!
  // Can refund already-refunded or cancelled orders
  // Can refund shipped orders that were never returned
  
  await stripe.refunds.create({
    charge: order.chargeId
  });
  
  res.json({ refunded: true });
});

// VULNERABLE: Price manipulation through referral
app.post('/apply-referral', async (req, res) => {
  const { referralCode } = req.body;
  
  // VULNERABLE: User controls their own referral bonus!
  // Attacker creates account, refers self, gets $50 credit
  // Repeats with multiple accounts
  
  const referrer = await db.users.findOne({ referralCode });
  
  if (referrer) {
    await db.users.update(
      { _id: referrer._id },
      { $inc: { credits: 50 } }
    );
    
    await db.users.update(
      { _id: req.user.id },
      { $inc: { credits: 50 } }
    );
  }
  
  res.json({ bonus: 50 });
});

// VULNERABLE: Workflow step bypass
app.post('/orders/:id/mark-shipped', async (req, res) => {
  const order = await db.orders.findById(req.params.id);
  
  // VULNERABLE: Doesn't verify payment completed!
  // Attacker marks unpaid order as shipped
  // Then claims "it was shipped, where's my refund?"
  
  await db.orders.update(
    { _id: order._id },
    { status: 'shipped' }
  );
  
  res.json({ success: true });
});
Secure
JAVASCRIPT
// Node.js/Express - Secure business logic

const rateLimit = require('express-rate-limit');

// SECURE: Server-side price catalog
const PRODUCT_CATALOG = {
  'prod-1': { name: 'Premium Plan', price: 99.99, maxQuantity: 1 },
  'prod-2': { name: 'Basic Plan', price: 19.99, maxQuantity: 1 },
  'prod-3': { name: 'Widget', price: 15.00, maxQuantity: 100 }
};

// SECURE: Server calculates total
function computeTotal(items) {
  let total = 0;
  
  for (const item of items) {
    // SECURE: Look up price from server catalog
    const product = PRODUCT_CATALOG[item.productId];
    
    if (!product) {
      throw new Error(`Invalid product: ${item.productId}`);
    }
    
    // SECURE: Validate quantity
    if (!Number.isInteger(item.quantity) || 
        item.quantity < 1 || 
        item.quantity > product.maxQuantity) {
      throw new Error(`Invalid quantity for ${product.name}`);
    }
    
    // SECURE: Server computes line total
    total += product.price * item.quantity;
  }
  
  return total;
}

// SECURE: Checkout with server-side validation
app.post('/checkout', async (req, res) => {
  try {
    const { items, couponCode } = req.body;
    
    // SECURE: Compute total server-side
    let total = computeTotal(items);
    
    // SECURE: Validate and apply coupon
    if (couponCode) {
      const discount = await applyCoupon(couponCode, req.user.id, total);
      total -= discount;
    }
    
    // SECURE: Charge with server-computed amount
    const charge = await stripe.charges.create({
      amount: Math.round(total * 100),  // Server's calculation!
      currency: 'usd',
      customer: req.user.stripeId
    });
    
    // Create order after successful payment
    const order = await db.orders.create({
      userId: req.user.id,
      items,
      total,
      chargeId: charge.id,
      status: 'paid'
    });
    
    res.json({ success: true, orderId: order.id, charged: total });
    
  } catch (err) {
    logger.error('Checkout error', { err, userId: req.user.id });
    res.status(400).json({ error: 'Checkout failed' });
  }
});

// SECURE: Add to cart with validation
app.post('/add-to-cart', async (req, res) => {
  const { productId, quantity } = req.body;
  
  const product = PRODUCT_CATALOG[productId];
  
  if (!product) {
    return res.status(404).json({ error: 'Product not found' });
  }
  
  // SECURE: Validate quantity is positive integer within limits
  if (!Number.isInteger(quantity) || 
      quantity < 1 || 
      quantity > product.maxQuantity) {
    return res.status(400).json({ 
      error: `Quantity must be between 1 and ${product.maxQuantity}` 
    });
  }
  
  await db.cart.insert({
    userId: req.user.id,
    productId,
    quantity,
    priceSnapshot: product.price  // Store price at add time
  });
  
  res.json({ success: true });
});

// SECURE: Coupon application with tracking
async function applyCoupon(code, userId, orderTotal) {
  const coupon = await db.coupons.findOne({ code });
  
  if (!coupon || !coupon.active) {
    throw new Error('Invalid coupon');
  }
  
  // SECURE: Check expiration
  if (coupon.expiresAt && new Date() > coupon.expiresAt) {
    throw new Error('Coupon expired');
  }
  
  // SECURE: Check minimum order amount
  if (coupon.minimumAmount && orderTotal < coupon.minimumAmount) {
    throw new Error(`Minimum order amount: $${coupon.minimumAmount}`);
  }
  
  // SECURE: Check if already used by this user
  const previousUse = await db.couponUsage.findOne({
    couponId: coupon._id,
    userId
  });
  
  if (previousUse) {
    throw new Error('Coupon already used');
  }
  
  // SECURE: Check total usage limit
  const totalUses = await db.couponUsage.countDocuments({
    couponId: coupon._id
  });
  
  if (coupon.maxUses && totalUses >= coupon.maxUses) {
    throw new Error('Coupon no longer available');
  }
  
  // Record usage
  await db.couponUsage.create({
    couponId: coupon._id,
    userId,
    usedAt: new Date()
  });
  
  // Calculate discount
  const discount = (orderTotal * coupon.percentage) / 100;
  return Math.min(discount, coupon.maxDiscount || Infinity);
}

// SECURE: Rate-limited report generation
const reportLimiter = rateLimit({
  windowMs: 60 * 1000,  // 1 minute
  max: 3,  // 3 reports per minute
  message: 'Too many report requests'
});

app.post('/generate-report', reportLimiter, async (req, res) => {
  const { startDate, endDate } = req.body;
  
  const start = new Date(startDate);
  const end = new Date(endDate);
  
  // SECURE: Limit date range to prevent resource exhaustion
  const daysDiff = (end - start) / (1000 * 60 * 60 * 24);
  if (daysDiff > 90) {
    return res.status(400).json({ error: 'Date range cannot exceed 90 days' });
  }
  
  // Generate report with pagination/limits
  const data = await db.transactions.find({
    userId: req.user.id,
    date: { $gte: start, $lte: end }
  }).limit(10000);  // Hard limit
  
  const report = generateReport(data);
  
  res.json({ report });
});

// SECURE: Purchase with stock checking and atomic operations
app.post('/purchase', async (req, res) => {
  const { productId, quantity } = req.body;
  
  // Start transaction
  const session = await db.startSession();
  session.startTransaction();
  
  try {
    // SECURE: Lock product row and check stock atomically
    const product = await db.products.findOneAndUpdate(
      { 
        _id: productId,
        stock: { $gte: quantity }  // Only if enough stock!
      },
      { 
        $inc: { stock: -quantity }  // Decrement atomically
      },
      { 
        session,
        new: true
      }
    );
    
    if (!product) {
      await session.abortTransaction();
      return res.status(400).json({ error: 'Insufficient stock' });
    }
    
    // Create order
    await db.orders.create([{
      userId: req.user.id,
      productId,
      quantity,
      price: product.price * quantity,
      status: 'pending'
    }], { session });
    
    await session.commitTransaction();
    
    res.json({ success: true });
    
  } catch (err) {
    await session.abortTransaction();
    throw err;
  } finally {
    session.endSession();
  }
});

// SECURE: State machine for order refunds
const ALLOWED_REFUND_STATES = ['delivered', 'shipped'];
const REFUND_WINDOW_DAYS = 30;

app.post('/orders/:id/refund', async (req, res) => {
  const order = await db.orders.findById(req.params.id);
  
  // SECURE: Verify ownership
  if (order.userId !== req.user.id) {
    return res.status(403).json({ error: 'Not your order' });
  }
  
  // SECURE: Check current state
  if (!ALLOWED_REFUND_STATES.includes(order.status)) {
    return res.status(400).json({ 
      error: `Cannot refund order with status: ${order.status}` 
    });
  }
  
  // SECURE: Check if already refunded
  if (order.refundedAt) {
    return res.status(400).json({ error: 'Order already refunded' });
  }
  
  // SECURE: Check refund window
  const daysSinceOrder = (Date.now() - order.createdAt) / (1000 * 60 * 60 * 24);
  if (daysSinceOrder > REFUND_WINDOW_DAYS) {
    return res.status(400).json({ 
      error: `Refund window of ${REFUND_WINDOW_DAYS} days has passed` 
    });
  }
  
  // Process refund
  await stripe.refunds.create({ charge: order.chargeId });
  
  // Update order state
  await db.orders.update(
    { _id: order._id },
    { 
      status: 'refunded',
      refundedAt: new Date()
    }
  );
  
  res.json({ success: true });
});

// SECURE: Referral with anti-abuse
app.post('/apply-referral', async (req, res) => {
  const { referralCode } = req.body;
  
  const referrer = await db.users.findOne({ referralCode });
  
  if (!referrer) {
    return res.status(404).json({ error: 'Invalid referral code' });
  }
  
  // SECURE: Prevent self-referral
  if (referrer._id.equals(req.user._id)) {
    return res.status(400).json({ error: 'Cannot refer yourself' });
  }
  
  // SECURE: Check if already used a referral
  if (req.user.referredBy) {
    return res.status(400).json({ error: 'Referral already applied' });
  }
  
  // SECURE: Detect abuse patterns (same IP, same device, etc.)
  const suspiciousActivity = await detectReferralAbuse(req.user, referrer);
  if (suspiciousActivity) {
    logger.warn('Suspicious referral', { userId: req.user.id, referrerId: referrer._id });
    return res.status(400).json({ error: 'Referral could not be applied' });
  }
  
  // Apply bonus
  await db.users.update(
    { _id: referrer._id },
    { $inc: { credits: 25 } }  // Reduced from vulnerable 50
  );
  
  await db.users.update(
    { _id: req.user._id },
    { 
      $inc: { credits: 25 },
      referredBy: referrer._id
    }
  );
  
  res.json({ bonus: 25 });
});

// SECURE: Workflow with state validation
const ORDER_STATE_MACHINE = {
  'pending': ['paid', 'cancelled'],
  'paid': ['processing', 'cancelled'],
  'processing': ['shipped', 'cancelled'],
  'shipped': ['delivered', 'returned'],
  'delivered': ['refunded'],
  'cancelled': [],
  'refunded': [],
  'returned': ['refunded']
};

app.post('/orders/:id/mark-shipped', requireAdmin, async (req, res) => {
  const order = await db.orders.findById(req.params.id);
  
  // SECURE: Verify current state allows this transition
  const allowedNext = ORDER_STATE_MACHINE[order.status];
  if (!allowedNext.includes('shipped')) {
    return res.status(400).json({ 
      error: `Cannot ship order with status: ${order.status}` 
    });
  }
  
  // SECURE: Verify payment completed
  if (order.status !== 'processing') {
    return res.status(400).json({ error: 'Order not ready to ship' });
  }
  
  await db.orders.update(
    { _id: order._id },
    { 
      status: 'shipped',
      shippedAt: new Date()
    }
  );
  
  res.json({ success: true });
});

Discovery

This vulnerability is discovered by analyzing application workflows to identify logic flaws such as missing validation of business rules, race conditions in multi-step processes, price manipulation through client-side values, or bypassing intended workflow sequences.

  1. 1. Observe normal checkout flow

    http

    Action

    Complete a legitimate purchase to understand the request structure and server response

    Request

    POST https://shop.example.com/checkout
    Headers:
    Content-Type: application/json
    Body:
    {
      "items": [
        {
          "sku": "PRO",
          "qty": 1
        }
      ],
      "total": 99.99
    }

    Response

    Status: 200
    Body:
    {
      "note": "200 OK with response: {ok: true, charged: 99.99}, confirming the checkout accepts a 'total' field from the client"
    }

    Artifacts

    http_response_body http_request_body
  2. 2. Test with modified total

    http

    Action

    Intercept the request using a proxy and change the 'total' field to a minimal value

    Request

    POST https://shop.example.com/checkout
    Headers:
    Content-Type: application/json
    Body:
    {
      "items": [
        {
          "sku": "PRO",
          "qty": 1
        }
      ],
      "total": 0.01
    }

    Response

    Status: 200
    Body:
    {
      "note": "200 OK with response: {ok: true, charged: 0.01}, confirming the server blindly trusts the client-provided total"
    }

    Artifacts

    http_response_body tampered_request
  3. 3. Verify payment processing

    analysis

    Action

    Check order confirmation and payment receipt to confirm the manipulated price was actually charged

    Request

    ANALYSIS N/A - Analysis step

    Response

    Status: 200
    Body:
    {
      "note": "Order confirmation shows $0.01 charged for PRO item (catalog price $99.99), proving the vulnerability affects real transactions"
    }

    Artifacts

    order_confirmation payment_receipt
  4. 4. Test boundary conditions

    http

    Action

    Experiment with edge cases like negative totals, zero, and extremely high values to understand validation limits

    Request

    POST https://shop.example.com/checkout
    Headers:
    Content-Type: application/json
    Body:
    {
      "items": [
        {
          "sku": "PRO",
          "qty": 1
        }
      ],
      "total": -10
    }

    Response

    Status: 200
    Body:
    {
      "note": "Negative totals may be rejected, but any positive value including 0.01 is accepted without server-side price verification"
    }

    Artifacts

    boundary_test_results

Exploit steps

An attacker exploits this by manipulating workflow sequences, submitting tampered prices or quantities, exploiting race conditions in concurrent operations, bypassing validation steps, or abusing promotional logic to gain unauthorized benefits or cause financial loss.

  1. 1. Execute single fraudulent purchase

    Buy high-value item for pennies

    http

    Action

    Submit checkout request for expensive PRO SKU ($99.99) with total set to $0.01

    Request

    POST https://shop.example.com/checkout
    Headers:
    Content-Type: application/json
    Body:
    {
      "items": [
        {
          "sku": "PRO",
          "qty": 1
        }
      ],
      "total": 0.01
    }

    Response

    Status: 200
    Body:
    {
      "note": "Order completed successfully: {ok: true, charged: 0.01}, saving $99.98 on a single transaction"
    }

    Artifacts

    http_response_body order_confirmation
  2. 2. Scale the attack

    Automate bulk purchases

    analysis

    Action

    Create script to repeatedly purchase high-value items with manipulated totals, varying quantities and using different shipping addresses to avoid detection

    Request

    ANALYSIS N/A - Analysis step

    Response

    Status: 200
    Body:
    {
      "note": "Successfully placed 50 orders for PRO items over 6 hours, spending $0.50 total for $4,999.50 worth of merchandise (99.99% discount)"
    }

    Artifacts

    automation_script order_batch_log
  3. 3. Drain inventory before detection

    Execute mass purchase campaign

    http

    Action

    Continue automated purchases across different accounts and payment methods to maximize inventory extraction

    Request

    POST https://shop.example.com/checkout
    Headers:
    Content-Type: application/json
    Body:
    {
      "items": [
        {
          "sku": "PRO",
          "qty": 5
        }
      ],
      "total": 0.05
    }

    Response

    Status: 200
    Body:
    {
      "note": "Processed 200+ fraudulent orders worth $500K+ in merchandise for under $10 before fraud detection systems flagged the anomaly"
    }

    Artifacts

    fraud_campaign_metrics inventory_depletion_log
  4. 4. Monetize stolen goods

    Resell acquired inventory

    analysis

    Action

    List fraudulently obtained items on secondary marketplaces at 50% retail value for quick liquidation

    Request

    ANALYSIS N/A - Analysis step

    Response

    Status: 200
    Body:
    {
      "note": "Generated $250K+ revenue from resale of items purchased for minimal cost, before merchant cancels and investigates orders"
    }

    Artifacts

    resale_listings revenue_log

Specific Impact

Orders complete at incorrect prices which leads to direct revenue loss and chargebacks when caught. Fraud rings can automate this to drain inventory quickly.

Support and finance reconciliation are impacted because records show legitimate paid orders, but at impossible prices.

Fix

Server recomputes totals from a trusted catalog and validates quantities.

This removes the attack surface where the client controls money values.

Detect This Vulnerability in Your Code

Sourcery automatically identifies business logic vulnerabilities and many other security issues in your codebase.

Scan Your Code for Free