Business Logic
Business Logic at a glance
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.
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.
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.
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.
// 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 });
});// 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. Observe normal checkout flow
httpAction
Complete a legitimate purchase to understand the request structure and server response
Request
POST https://shop.example.com/checkoutHeaders:Content-Type: application/jsonBody:{ "items": [ { "sku": "PRO", "qty": 1 } ], "total": 99.99 }Response
Status: 200Body:{ "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. Test with modified total
httpAction
Intercept the request using a proxy and change the 'total' field to a minimal value
Request
POST https://shop.example.com/checkoutHeaders:Content-Type: application/jsonBody:{ "items": [ { "sku": "PRO", "qty": 1 } ], "total": 0.01 }Response
Status: 200Body:{ "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. Verify payment processing
analysisAction
Check order confirmation and payment receipt to confirm the manipulated price was actually charged
Request
ANALYSIS N/A - Analysis stepResponse
Status: 200Body:{ "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. Test boundary conditions
httpAction
Experiment with edge cases like negative totals, zero, and extremely high values to understand validation limits
Request
POST https://shop.example.com/checkoutHeaders:Content-Type: application/jsonBody:{ "items": [ { "sku": "PRO", "qty": 1 } ], "total": -10 }Response
Status: 200Body:{ "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. Execute single fraudulent purchase
Buy high-value item for pennies
httpAction
Submit checkout request for expensive PRO SKU ($99.99) with total set to $0.01
Request
POST https://shop.example.com/checkoutHeaders:Content-Type: application/jsonBody:{ "items": [ { "sku": "PRO", "qty": 1 } ], "total": 0.01 }Response
Status: 200Body:{ "note": "Order completed successfully: {ok: true, charged: 0.01}, saving $99.98 on a single transaction" }Artifacts
http_response_body order_confirmation -
2. Scale the attack
Automate bulk purchases
analysisAction
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 stepResponse
Status: 200Body:{ "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. Drain inventory before detection
Execute mass purchase campaign
httpAction
Continue automated purchases across different accounts and payment methods to maximize inventory extraction
Request
POST https://shop.example.com/checkoutHeaders:Content-Type: application/jsonBody:{ "items": [ { "sku": "PRO", "qty": 5 } ], "total": 0.05 }Response
Status: 200Body:{ "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. Monetize stolen goods
Resell acquired inventory
analysisAction
List fraudulently obtained items on secondary marketplaces at 50% retail value for quick liquidation
Request
ANALYSIS N/A - Analysis stepResponse
Status: 200Body:{ "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