Business Logic Vulnerabilities
Business logic vulnerabilities occur when application design flaws allow attackers to abuse legitimate functionality in unintended ways. Unlike technical vulnerabilities that exploit code weaknesses, these attacks manipulate how applications are supposed to work - making them difficult to detect with automated scanners and invisible to traditional security controls.
What Makes Business Logic Flaws Different
Business logic vulnerabilities arise from flawed assumptions about how users will interact with an application. They don't involve malformed input or exploitation of technical weaknesses - instead, attackers use the application exactly as designed, just not as intended.
Key characteristics:
- Automated scanners cannot detect them
- Require understanding of business context
- WAFs and security tools don't block them
- Often have severe financial or operational impact
Common Attack Patterns
Price Manipulation
// Vulnerable checkout flow
app.post('/api/checkout', async (req, res) => {
const { items, total } = req.body;
// VULNERABLE: Trusting client-provided total
await processPayment(req.user.id, total);
await createOrder(req.user.id, items);
res.json({ success: true });
});
// Attack: Frontend sends cart items with a modified total
// POST /api/checkout
// { "items": [{"id": "expensive-item", "qty": 1}], "total": 0.01 }
// Attacker pays $0.01 for a $999 itemSecure: Server-Side Price Calculation
// Secure: Calculate total on server
app.post('/api/checkout', async (req, res) => {
const { items } = req.body;
// Calculate total from database prices
let total = 0;
for (const item of items) {
const product = await db.products.findById(item.id);
if (!product) {
return res.status(400).json({ error: `Invalid product: ${item.id}` });
}
total += product.price * item.qty;
}
// Process with server-calculated total
await processPayment(req.user.id, total);
await createOrder(req.user.id, items, total);
res.json({ success: true, total });
});Workflow Bypass
// Vulnerable: Multi-step process without state validation
// Step 1: Add items to cart
app.post('/api/cart/add', (req, res) => {
// ... add to cart
});
// Step 2: Enter shipping address
app.post('/api/checkout/shipping', (req, res) => {
// ... save shipping
});
// Step 3: Process payment
app.post('/api/checkout/payment', (req, res) => {
// ... process payment
});
// Step 4: Complete order (VULNERABLE)
app.post('/api/checkout/complete', async (req, res) => {
// No validation that previous steps were completed!
await createOrder(req.session.cartId);
res.json({ success: true });
});
// Attack: Skip directly to /api/checkout/complete
// Bypasses payment step entirelySecure: State Machine Validation
// Secure: Validate workflow state
const CHECKOUT_STATES = {
CART: 'cart',
SHIPPING: 'shipping',
PAYMENT: 'payment',
COMPLETED: 'completed'
};
const VALID_TRANSITIONS = {
[CHECKOUT_STATES.CART]: [CHECKOUT_STATES.SHIPPING],
[CHECKOUT_STATES.SHIPPING]: [CHECKOUT_STATES.PAYMENT],
[CHECKOUT_STATES.PAYMENT]: [CHECKOUT_STATES.COMPLETED],
};
async function validateTransition(checkoutId, targetState) {
const checkout = await db.checkouts.findById(checkoutId);
const validNextStates = VALID_TRANSITIONS[checkout.state] || [];
if (!validNextStates.includes(targetState)) {
throw new Error(`Invalid transition: ${checkout.state} -> ${targetState}`);
}
return checkout;
}
app.post('/api/checkout/complete', async (req, res) => {
try {
// Verify payment step was completed
const checkout = await validateTransition(
req.session.checkoutId,
CHECKOUT_STATES.COMPLETED
);
// Also verify payment was actually processed
if (!checkout.paymentId) {
return res.status(400).json({ error: 'Payment not processed' });
}
await createOrder(checkout);
res.json({ success: true });
} catch (error) {
res.status(400).json({ error: error.message });
}
});Coupon and Discount Abuse
// Vulnerable: Multiple discount stacking
app.post('/api/cart/apply-coupon', async (req, res) => {
const { couponCode } = req.body;
const cart = await getCart(req.user.id);
const coupon = await db.coupons.findByCode(couponCode);
if (!coupon || coupon.expired) {
return res.status(400).json({ error: 'Invalid coupon' });
}
// VULNERABLE: No check for existing discounts
cart.appliedCoupons.push(coupon);
// Apply all discounts
let discount = 0;
for (const c of cart.appliedCoupons) {
discount += c.discountPercent;
}
// Attack: Stack multiple 50% coupons for free items
cart.discount = Math.min(discount, 100); // Caps at 100% but still exploitable
await cart.save();
res.json(cart);
});
// Attack flow:
// 1. Apply SUMMER50 (50% off)
// 2. Apply WELCOME50 (50% off)
// 3. Get 100% off on everythingSecure: Discount Rules Engine
// Secure: Explicit discount rules
const DISCOUNT_RULES = {
maxCouponsPerOrder: 1,
maxDiscountPercent: 50,
incompatibleCoupons: [
['SUMMER50', 'WELCOME50'], // Can't use together
],
};
app.post('/api/cart/apply-coupon', async (req, res) => {
const { couponCode } = req.body;
const cart = await getCart(req.user.id);
// Check coupon limit
if (cart.appliedCoupons.length >= DISCOUNT_RULES.maxCouponsPerOrder) {
return res.status(400).json({
error: 'Maximum coupon limit reached'
});
}
const coupon = await validateCoupon(couponCode, cart);
// Check incompatibility
for (const existing of cart.appliedCoupons) {
if (areIncompatible(existing.code, couponCode)) {
return res.status(400).json({
error: 'This coupon cannot be combined with existing discounts'
});
}
}
// Calculate with cap
const totalDiscount = calculateTotalDiscount(cart, coupon);
if (totalDiscount > DISCOUNT_RULES.maxDiscountPercent) {
return res.status(400).json({
error: 'Maximum discount exceeded'
});
}
await applyDiscount(cart, coupon);
res.json(cart);
});Inventory Manipulation
// Vulnerable: Reservation without time limit
app.post('/api/cart/reserve', async (req, res) => {
const { productId, quantity } = req.body;
const product = await db.products.findById(productId);
if (product.stock < quantity) {
return res.status(400).json({ error: 'Out of stock' });
}
// VULNERABLE: Reserve indefinitely without checkout
product.stock -= quantity;
await product.save();
req.session.reserved.push({ productId, quantity });
res.json({ success: true });
});
// Attack: Reserve all inventory across multiple sessions
// Other customers see "out of stock"
// Attacker never completes purchaseSecure: Time-Limited Reservations
// Secure: Expire reservations automatically
const RESERVATION_TTL = 15 * 60 * 1000; // 15 minutes
app.post('/api/cart/reserve', async (req, res) => {
const { productId, quantity } = req.body;
// Atomic reservation with expiry
const result = await db.reservations.create({
userId: req.user.id,
productId,
quantity,
expiresAt: new Date(Date.now() + RESERVATION_TTL),
});
if (!result.success) {
return res.status(400).json({ error: 'Unable to reserve' });
}
res.json({
success: true,
expiresAt: result.expiresAt
});
});
// Background job: Release expired reservations
cron.schedule('*/5 * * * *', async () => {
await db.reservations.deleteMany({
expiresAt: { $lt: new Date() }
});
});
// Real stock = physical_stock - active_reservationsNegative Value Exploitation
// Vulnerable: No validation of quantity sign
app.post('/api/cart/update', async (req, res) => {
const { productId, quantity } = req.body;
const cart = await getCart(req.user.id);
// VULNERABLE: Accepts negative quantities
const item = cart.items.find(i => i.productId === productId);
item.quantity = quantity;
// Total calculation
cart.total = cart.items.reduce(
(sum, item) => sum + (item.price * item.quantity),
0
);
await cart.save();
res.json(cart);
});
// Attack: Set quantity to -1
// POST /api/cart/update { "productId": "item1", "quantity": -1 }
// Item costs $100, so cart total becomes -$100
// Store owes the attacker money!Secure: Input Validation with Business Rules
import { z } from 'zod';
const updateCartSchema = z.object({
productId: z.string().uuid(),
quantity: z.number().int().min(0).max(100),
});
app.post('/api/cart/update', async (req, res) => {
const result = updateCartSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ error: result.error.issues });
}
const { productId, quantity } = result.data;
// Additional business rule validation
const product = await db.products.findById(productId);
if (quantity > product.maxPerOrder) {
return res.status(400).json({
error: `Maximum ${product.maxPerOrder} per order`
});
}
// Update cart safely
await updateCartItem(req.user.id, productId, quantity);
res.json(await getCart(req.user.id));
});Security Checklist
- Never trust client-provided prices, totals, or calculations
- Implement state machine validation for multi-step workflows
- Define explicit rules for discounts, limits, and combinations
- Add time limits to reservations and pending actions
- Validate numeric inputs for range, sign, and reasonability
- Log business logic operations for anomaly detection
- Conduct threat modeling focused on business flows
- Perform manual testing by security engineers who understand the business
Practice Challenges
View allNegative Quantity
Shopping cart accepts negative quantities. Profit!
Coupon Reuse
Discount coupon with no usage limit check.
Parameter Tampering
Price in the request. What if you change it?
Workflow Bypass
Multi-step checkout. Skip the payment step.
Referral Abuse
Referral program. Refer yourself for infinite rewards.