Business Logic Vulnerabilities: Your App Works As Designed (For Hackers) 🧠
Business Logic Vulnerabilities: Your App Works As Designed (For Hackers) 🧠
Imagine spending two weeks hardening your app against SQL injection. You've got parameterized queries, WAF rules, security headers — the works. Then someone orders $500 worth of products for -$47.50 because your discount input accepts negative numbers.
No CVE for that. No Metasploit module. Just your business logic, being a helpful accomplice to fraud.
Welcome to business logic vulnerabilities — the class of bugs that scanners can't find, WAFs can't block, and penetration testers love to find in final reports.
What Even Is a Business Logic Vulnerability? 🤔
A business logic flaw is when an attacker uses your application's intended functionality in an unintended way.
No injection. No memory corruption. No zero-days. Just your carefully designed features, being misused in ways your dev team never considered in standup.
As someone who spent years architecting e-commerce backends on AWS, I've seen these everywhere. They're invisible until they're catastrophic.
The Price Manipulation Classic 💸
The most painful one I've encountered personally. Building a serverless e-commerce backend, we had a discount code system. Totally standard.
The Terrible Code (don't do this):
// "What if discount is negative?" - Nobody, ever
const finalPrice = originalPrice - discountAmount;
What a creative user typed: A discount code that mapped to -500 as the amount.
Result: Cart total became negative. Stripe happily processed a "payment" of -$47.50. Customer got their order AND a credit back to their card. We noticed three days later.
The Fix:
// Validate that discounts only ever REDUCE the price
const discountAmount = Math.abs(parsedDiscount); // Ensure positive
const finalPrice = Math.max(0, originalPrice - discountAmount); // Never below zero
if (finalPrice <= 0) {
throw new Error("Invalid discount configuration");
}
Pro Tip: Always define business constraints explicitly. "Price must be positive" isn't obvious to your discount calculation code.
Workflow Bypass — Skipping Steps That Matter 🏃♂️
In security communities, we often discuss this one as the "checkout bypass." Your checkout has steps:
1. Add items to cart
2. Enter shipping details
3. Enter payment details
4. Confirm order ← "what if I jump here directly?"
A classic bug: the server only checks that a session exists, not that each prior step was completed.
Real Talk: I audited an e-commerce flow once where hitting /order/confirm with a valid session ID — but skipping the payment step — would still create the order. The payment intent check was purely client-side JavaScript. The server trusted the frontend.
The Fix: Keep state on the server.
// Bad: trusting the client about payment status
if ($request->input('payment_completed') === true) {
// process order
}
// Good: verify server-side that payment intent was created AND confirmed
$paymentIntent = PaymentIntent::retrieve($session->payment_intent_id);
if ($paymentIntent->status !== 'succeeded') {
abort(402, 'Payment not completed');
}
Your frontend is a suggestion. Your backend is law.
The Coupon Code Abuse Spiral 🎟️
"One coupon per user" is a business rule. It's often NOT a technical rule.
In my experience building production systems, coupon validation frequently looks like:
// Checks if THIS email used the coupon before
const used = await CouponUsage.findOne({ email, couponCode });
if (used) throw new Error("Coupon already used");
The bypass: [email protected], [email protected], [email protected] — all different emails to most systems, all the same inbox in Gmail.
Or: create a free account with a throwaway email for each use.
Real Fix:
// Tie usage to ACCOUNT, not email — and add device/IP heuristics
const used = await CouponUsage.findOne({
userId: authenticatedUser.id, // NOT email
couponCode
});
// Also limit by IP for guest checkouts
const ipUsage = await CouponUsage.count({
ipAddress: req.ip,
couponCode,
createdAt: { $gte: last24Hours }
});
if (used || ipUsage >= 3) throw new Error("Coupon limit reached");
Integer Overflow — The "Too Many Coupons" Trick 🔢
Less common in modern languages, but it still happens in financial calculations.
The bug: Apply the same $10 coupon 100 times. If the system accumulates discounts without a cap:
$50 item - $10 × 100 coupons = -$950 "owed to customer"
Some older systems (or systems ported from C) would overflow an integer here and wrap around to a massive positive number, crashing payment processors in hilarious ways.
The Fix: Always cap discounts at originalPrice and validate totals before charging.
const maxDiscount = originalPrice; // Can't discount more than you charge
const appliedDiscount = Math.min(totalDiscounts, maxDiscount);
const finalPrice = originalPrice - appliedDiscount;
console.assert(finalPrice >= 0, "Final price sanity check failed");
Race Conditions in Business Logic 🏁
"One referral bonus per referred user" — simple rule, right?
A user refers a friend. Two requests arrive simultaneously: one from the referrer claiming the bonus, one from the new user completing signup. Both checks see "bonus not claimed yet." Both grant the bonus. Double payout.
The Fix: Use database-level locks or atomic operations:
// Laravel: Use atomic transactions with a DB-level constraint
DB::transaction(function () use ($referralCode) {
$referral = Referral::lockForUpdate()
->where('code', $referralCode)
->where('status', 'pending')
->firstOrFail();
$referral->update(['status' => 'claimed']);
// Only reaches here if we got the lock
$this->grantBonus($referral->referrer_id);
});
The lockForUpdate() ensures only one request wins the race.
Feature Envy — Abusing Intended Features 🎭
As someone passionate about security, this is my favorite category to find in bug bounties.
Refund abuse: Some systems allow refunds on "digital goods" indefinitely. Attacker buys, uses, refunds, repeats.
Review bombing via accounts: Platforms that allow one review per purchase, but don't prevent the same person from making multiple purchases specifically to leave reviews.
API parameter tampering:
POST /api/transfer
{"to_account": "12345", "amount": 100}
What if you send: {"to_account": "12345", "amount": 100, "currency": "JPY"} when the UI only ever sends USD? Does the backend validate the currency, or does it default to something unexpected?
Real Talk: In security communities, we call this "parameter tampering." The attacker didn't inject anything — they just added a field the developer didn't think to validate because the UI never sends it.
How to Actually Prevent This 🛡️
Traditional scanners won't save you here. You need a different mindset:
1. Threat model your business flows. For every feature, ask: "What if someone sends this input twice? What if they skip step 2? What if the number is negative? What if it's 999999999?"
2. Server-side validation for ALL business rules. The frontend is decoration. The backend enforces reality.
3. Add integration tests for abuse scenarios:
it('should not allow negative discount amounts', async () => {
const response = await request(app)
.post('/apply-coupon')
.send({ code: 'HACK', amount: -500 });
expect(response.status).toBe(400);
expect(response.body.total).toBeGreaterThan(0);
});
4. Log suspicious patterns. Multiple coupon applications in seconds, unusual order totals, skipped workflow steps — set up alerts.
5. Review your financial flows with a "how would I abuse this?" hat on. Ask your QA team (or a friendly security researcher) to play attacker.
Real Talk 💬
Q: "My app is small, attackers won't bother."
Wrong. These attacks are often automated. Someone writes a script that tries negative coupons on every e-commerce site they can find. Small apps get hit first because they're less hardened.
Q: "Wouldn't a pen tester catch this?"
Automated pen testers won't. Manual testers will — if you tell them to review business logic. Many pentests focus on injection and auth bypasses. Ask specifically for business logic testing.
Q: "This sounds like it requires attacker creativity, not just running tools."
Exactly. That's why it's so dangerous. And why human code reviewers asking "what can go wrong here?" matter more than any scanner.
Your Business Logic Security Checklist 📋
- All financial calculations validated server-side (prices, discounts, totals)
- Discounts/coupons can never make total negative
- Workflow steps enforced server-side, not just client-side
- Rate limiting on coupon/promo code attempts
- Coupon usage tied to user account, not just email
- Race conditions in bonus/referral systems handled with DB locks
- Input validation for fields the UI doesn't normally send
- Abuse scenario tests in your test suite
- Anomaly alerting on unusual financial patterns
TL;DR
Business logic vulnerabilities don't appear in CVE databases. No scanner will find them. They're the bugs only you can prevent — by thinking like an attacker about your own features.
In my experience building production e-commerce systems on AWS: the expensive incidents weren't injection attacks. They were clever users doing exactly what the app allowed, just in combinations nobody imagined.
Your WAF stops the 1000 obvious attacks. This is the one you have to think about yourself.
Found a business logic flaw? I'd love to hear about it (responsibly, of course). Hit me up on LinkedIn or check out my other security posts on GitHub.
Think like an attacker. Build like a defender. 🛡️