Skip to main content
Esprit CLI detects business logic flaws that exploit intended functionality to violate domain invariants. Unlike injection attacks, these vulnerabilities abuse legitimate features to achieve unauthorized outcomes like financial fraud, privilege retention, or workflow bypass.
Business logic flaws require understanding the business domain, not just payload catalogs. Every invariant must be enforced server-side.

Attack Surface Coverage

Critical Business Logic

  • Financial flows - Pricing, payments, refunds, credits, discounts
  • Account lifecycle - Signup, trials, upgrades, suspensions
  • Authorization workflows - Approval chains, role transitions
  • Quotas and limits - Rate limits, usage caps, inventory
  • Multi-tenant isolation - Cross-organization data/action bleed
  • Event-driven flows - Jobs, webhooks, sagas, idempotency

High-Value Targets

Esprit prioritizes testing:

Pricing and Payments

  • Price manipulation in cart/checkout
  • Discount stacking and mutual exclusivity
  • Tax and shipping calculation
  • Currency arbitrage
  • Payment capture/void/refund sequences
  • Partial refunds exceeding original amount

Credits and Vouchers

  • Gift card/credit issuance and redemption
  • Multiple redemption of single-use codes
  • Balance manipulation
  • Expiry bypass

Subscriptions

  • Trial extension
  • Upgrade/downgrade proration
  • Seat count manipulation
  • Metering and usage reporting

Quotas and Rate Limits

  • Daily/monthly usage limits
  • Inventory reservation leaks
  • Distributed counter inconsistencies

State Machine Vulnerabilities

Esprit detects workflow bypass:

Step Skipping

// Vulnerable: Multi-step checkout process
// Step 1: POST /cart/items
app.post('/cart/items', authenticate, async (req, res) => {
  const item = await addToCart(req.user.id, req.body);
  res.json({ cartId: item.cartId });
});

// Step 2: POST /cart/shipping
app.post('/cart/shipping', authenticate, async (req, res) => {
  await setShipping(req.user.id, req.body);
  res.json({ success: true });
});

// Step 3: POST /cart/finalize - VULNERABLE
app.post('/cart/finalize', authenticate, async (req, res) => {
  // VULNERABLE: No validation that shipping was set!
  const order = await finalizeCart(req.user.id);
  res.json({ orderId: order.id });
});
Exploitation:
# Skip shipping step to avoid shipping charges
curl -X POST /cart/items -d '{"productId": "prod_123", "qty": 1}'
curl -X POST /cart/finalize  # Skip /cart/shipping entirely

State Replay

// Vulnerable: Apply discount endpoint
app.post('/cart/discount', authenticate, async (req, res) => {
  const { code } = req.body;
  const discount = await Discount.findOne({ code });
  
  // VULNERABLE: No check if already applied
  await Cart.updateOne(
    { userId: req.user.id },
    { $inc: { discount: discount.amount } }
  );
  
  res.json({ success: true });
});
Exploitation:
# Apply same discount code multiple times
for i in {1..10}; do
  curl -X POST /cart/discount -d '{"code": "SAVE20"}'
done

Out-of-Order Execution

// Vulnerable: Refund before capture
app.post('/payments/:id/refund', authenticate, async (req, res) => {
  const payment = await Payment.findById(req.params.id);
  
  // VULNERABLE: No state validation
  await payment.update({ status: 'refunded' });
  await issueRefund(payment.amount);
  
  res.json({ success: true });
});
Exploitation:
# Refund payment that was only authorized, not captured
curl -X POST /payments/pay_pending/refund
# Result: Free money without deducting from captured funds

Concurrency Vulnerabilities

Esprit tests race conditions:

Parallel Resource Consumption

// Vulnerable: Credit redemption
app.post('/credits/redeem', authenticate, async (req, res) => {
  const user = await User.findById(req.user.id);
  
  if (user.credits >= 10) {
    // VULNERABLE: Non-atomic check-then-act
    await User.updateOne(
      { _id: req.user.id },
      { $inc: { credits: -10 } }
    );
    
    await grantPremiumFeature(req.user.id);
    res.json({ success: true });
  } else {
    res.status(400).json({ error: 'Insufficient credits' });
  }
});
Exploitation:
# Send 10 parallel requests with 10 credits available
# Result: Premium feature granted 10 times, credits go negative
seq 10 | xargs -P10 -I{} curl -X POST /credits/redeem
Esprit detection: Sends parallel requests to detect race conditions

Idempotency Bypass

// Vulnerable: Idempotency key scoped incorrectly
app.post('/payments/charge', authenticate, async (req, res) => {
  const idempotencyKey = req.headers['idempotency-key'];
  
  // VULNERABLE: Key not scoped to user
  const cached = await cache.get(idempotencyKey);
  if (cached) return res.json(cached);
  
  const charge = await processPayment(req.body);
  await cache.set(idempotencyKey, charge, 3600);
  
  res.json(charge);
});
Exploitation:
# User A processes payment with key "abc123"
curl -X POST /payments/charge \
  -H "Idempotency-Key: abc123" \
  -H "Authorization: Bearer user-a-token" \
  -d '{"amount": 100}'

# User B reuses same key to get User A's result
curl -X POST /payments/charge \
  -H "Idempotency-Key: abc123" \
  -H "Authorization: Bearer user-b-token" \
  -d '{"amount": 999}'
# Result: Returns User A's charge info to User B

Numeric and Currency Issues

Esprit detects calculation flaws:

Price Manipulation

// Vulnerable: Client-computed total
app.post('/orders/create', authenticate, async (req, res) => {
  const { items, total } = req.body;
  
  // VULNERABLE: Trusts client-provided total
  const order = await Order.create({
    userId: req.user.id,
    items,
    total  // Should be server-computed!
  });
  
  res.json(order);
});
Exploitation:
# Send manipulated total
curl -X POST /orders/create -d '{
  "items": [
    {"id": "prod_123", "price": 99.99, "qty": 10}
  ],
  "total": 0.01
}'

Rounding Errors

// Vulnerable: Rounding in user's favor
app.post('/cart/discount', authenticate, async (req, res) => {
  const cart = await Cart.findOne({ userId: req.user.id });
  
  // Per-item discount with rounding
  for (const item of cart.items) {
    // VULNERABLE: Rounding down each item
    item.discount = Math.floor(item.price * 0.1);
  }
  
  await cart.save();
  res.json(cart);
});
Exploitation:
# Add many low-price items to maximize rounding loss
# 100 items at $1.00 each
# Expected discount: $10.00
# Actual discount: $0 (Math.floor(1.00 * 0.1) = 0 per item)

Currency Arbitrage

// Vulnerable: Stale exchange rates
app.post('/payments/currency-switch', authenticate, async (req, res) => {
  const { fromCurrency, toCurrency } = req.body;
  const balance = await getBalance(req.user.id, fromCurrency);
  
  // VULNERABLE: Uses cached/stale rate
  const rate = await getExchangeRate(fromCurrency, toCurrency);
  const convertedAmount = balance * rate;
  
  await setBalance(req.user.id, fromCurrency, 0);
  await setBalance(req.user.id, toCurrency, convertedAmount);
  
  res.json({ success: true });
});
Exploitation:
# Abuse rate staleness or rounding
# Convert USD -> EUR -> USD repeatedly
# Gain value from rounding or delayed rate updates

Feature Gate Bypass

Esprit detects authorization logic flaws:

Client-Side Feature Flags

// Vulnerable: Feature flag checked client-side only
// Client code:
if (user.plan === 'premium') {
  showAdvancedFeature();
}

// API endpoint - VULNERABLE
app.post('/api/advanced-feature', authenticate, async (req, res) => {
  // No server-side plan check!
  const result = await executeAdvancedFeature(req.user.id, req.body);
  res.json(result);
});
Exploitation:
# Basic user calls premium endpoint directly
curl -X POST /api/advanced-feature \
  -H "Authorization: Bearer basic-user-token" \
  -d '{"data": "..."}

Role Transition Leaks

// Vulnerable: Cached permissions after downgrade
app.post('/users/downgrade', authenticate, async (req, res) => {
  await User.updateOne(
    { _id: req.user.id },
    { $set: { plan: 'basic' } }
  );
  
  // VULNERABLE: Doesn't invalidate cached permissions
  res.json({ success: true });
});

// Premium feature check
app.post('/api/premium-feature', authenticate, async (req, res) => {
  // VULNERABLE: Uses cached user object
  if (req.user.plan === 'premium') {  // Stale!
    return res.json(await executePremiumFeature());
  }
  res.status(403).json({ error: 'Premium required' });
});

Example Scenarios

Scenario 1: Double Refund

// Vulnerable code: src/api/refunds.js:45
app.post('/refunds/create', authenticate, async (req, res) => {
  const { orderId, amount } = req.body;
  
  const order = await Order.findById(orderId);
  
  if (order.userId !== req.user.id) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  
  // VULNERABLE: No check for existing refunds
  const refund = await Refund.create({
    orderId,
    amount,
    userId: req.user.id
  });
  
  await issueRefundToPaymentMethod(refund);
  res.json(refund);
});
Exploitation:
# Refund via customer portal
curl -X POST /refunds/create -d '{"orderId": "ord_123", "amount": 99.99}'

# Also contact support who issues manual refund
# Result: Double refund totaling $199.98 for $99.99 order
Impact: Direct financial loss

Scenario 2: Inventory Reservation Leak

// Vulnerable code: src/services/inventory.js:78
app.post('/cart/reserve', authenticate, async (req, res) => {
  const { productId, quantity } = req.body;
  
  const available = await Inventory.findOne({ productId });
  
  if (available.quantity >= quantity) {
    // VULNERABLE: Reserves without expiry or cleanup
    await Inventory.updateOne(
      { productId },
      { $inc: { quantity: -quantity, reserved: quantity } }
    );
    
    await Cart.create({
      userId: req.user.id,
      productId,
      quantity
    });
    
    res.json({ success: true });
  }
});
Exploitation:
# Repeatedly add to cart without completing purchase
for i in {1..1000}; do
  curl -X POST /cart/reserve -d '{"productId": "prod_123", "qty": 1}'
done
# Result: Inventory locked by abandoned reservations
# Legitimate customers see "out of stock"
Impact: Denial of inventory, revenue loss

Remediation

Esprit recommends:

Enforce State Machine Transitions

// SAFE: Validate preconditions
const STATE_MACHINE = {
  pending: ['processing'],
  processing: ['completed', 'failed'],
  completed: ['refunded'],
  failed: ['pending'],
  refunded: []
};

app.post('/orders/:id/transition', authenticate, async (req, res) => {
  const { targetState } = req.body;
  const order = await Order.findById(req.params.id);
  
  // Validate ownership
  if (order.userId !== req.user.id) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  
  // Validate transition
  const allowedTransitions = STATE_MACHINE[order.state] || [];
  if (!allowedTransitions.includes(targetState)) {
    return res.status(400).json({ 
      error: `Cannot transition from ${order.state} to ${targetState}` 
    });
  }
  
  order.state = targetState;
  await order.save();
  
  res.json(order);
});

Atomic Operations

// SAFE: Atomic check-and-update
app.post('/credits/redeem', authenticate, async (req, res) => {
  const result = await User.updateOne(
    { 
      _id: req.user.id,
      credits: { $gte: 10 }  // Atomic check
    },
    { 
      $inc: { credits: -10 }  // Atomic update
    }
  );
  
  if (result.modifiedCount === 0) {
    return res.status(400).json({ error: 'Insufficient credits' });
  }
  
  await grantPremiumFeature(req.user.id);
  res.json({ success: true });
});

Server-Side Computation

// SAFE: Server computes all financial values
app.post('/orders/create', authenticate, async (req, res) => {
  const { items } = req.body;
  
  // Fetch current prices from database
  const products = await Product.find({
    _id: { $in: items.map(i => i.productId) }
  });
  
  // Server computes subtotal
  let subtotal = 0;
  for (const item of items) {
    const product = products.find(p => p.id === item.productId);
    subtotal += product.price * item.quantity;
  }
  
  // Apply discounts (server-side validation)
  const discount = await calculateDiscount(req.user.id, items);
  
  // Calculate tax (server-side)
  const tax = await calculateTax(subtotal - discount, req.user.address);
  
  // Final total
  const total = subtotal - discount + tax;
  
  const order = await Order.create({
    userId: req.user.id,
    items,
    subtotal,
    discount,
    tax,
    total
  });
  
  res.json(order);
});

Proper Idempotency

// SAFE: User-scoped idempotency keys
app.post('/payments/charge', authenticate, async (req, res) => {
  const idempotencyKey = req.headers['idempotency-key'];
  
  if (!idempotencyKey) {
    return res.status(400).json({ error: 'Idempotency-Key required' });
  }
  
  // Scope key to user
  const cacheKey = `payment:${req.user.id}:${idempotencyKey}`;
  
  const cached = await cache.get(cacheKey);
  if (cached) return res.json(cached);
  
  const charge = await processPayment({
    userId: req.user.id,
    ...req.body
  });
  
  // Cache with TTL
  await cache.set(cacheKey, charge, 86400);
  
  res.json(charge);
});

Validate Invariants

// SAFE: Check domain invariants
app.post('/refunds/create', authenticate, async (req, res) => {
  const { orderId, amount } = req.body;
  
  const order = await Order.findById(orderId);
  
  // Check ownership
  if (order.userId !== req.user.id) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  
  // Check order state
  if (order.state !== 'completed') {
    return res.status(400).json({ error: 'Order not completed' });
  }
  
  // Calculate existing refunds
  const existingRefunds = await Refund.aggregate([
    { $match: { orderId: order._id } },
    { $group: { _id: null, total: { $sum: '$amount' } } }
  ]);
  
  const totalRefunded = existingRefunds[0]?.total || 0;
  
  // Validate invariant: total refunds <= order total
  if (totalRefunded + amount > order.total) {
    return res.status(400).json({ 
      error: 'Refund exceeds order total',
      orderTotal: order.total,
      alreadyRefunded: totalRefunded,
      requested: amount
    });
  }
  
  const refund = await Refund.create({
    orderId,
    amount,
    userId: req.user.id
  });
  
  await issueRefundToPaymentMethod(refund);
  res.json(refund);
});
Esprit validates that all business invariants are enforced at the service layer, not just at the API gateway or UI.

Detection Output

Esprit provides detailed business logic findings:
[HIGH] State machine bypass in checkout flow
Location: src/api/checkout.js:89
Invariant Violated: Shipping required before finalization

Vulnerable Flow:
  POST /cart/items → 201 Created
  POST /cart/finalize → 200 OK (SKIPPED /cart/shipping)

Expected Flow:
  POST /cart/items → POST /cart/shipping → POST /cart/finalize

Impact:
  - Zero shipping charges
  - Orders shipped without address validation
  - Per-order loss: $5-15 shipping fee
  - Scalable via automation

Proof:
  Order ord_123 created with:
    - Subtotal: $99.99
    - Shipping: $0.00 (should be $8.99)
    - Total: $99.99 (should be $108.98)
  
  Shipping address: null
  Order status: confirmed

Remediation:
  app.post('/cart/finalize', authenticate, async (req, res) => {
    const cart = await Cart.findOne({ userId: req.user.id });
    
    // Validate preconditions
    if (!cart.shippingAddress) {
      return res.status(400).json({ 
        error: 'Shipping address required' 
      });
    }
    
    // Continue with finalization...
  });

Next Steps

IDOR Detection

Detect object-level authorization issues

SQL Injection

Find database injection vulnerabilities

Build docs developers (and LLMs) love