Business Logic Vulnerabilities & IDOR

Identify and exploit business logic flaws and Insecure Direct Object References — vulnerabilities that scanners miss because they require understanding how the application is supposed to work.

Medium 60m 3 tasks

Learning Objectives

  • Identify IDOR in ID parameters, GUIDs, and indirect references
  • Exploit business logic to bypass price validation, workflow steps, and access controls
  • Chain multiple low-severity flaws into a high-impact attack
  • Implement authorization checks for every object access
  • Test for horizontal and vertical privilege escalation

Insecure Direct Object Reference (IDOR)

What is IDOR?

Application exposes internal object IDs — user can access/modify other users' objects by changing the ID:

GET /api/invoice/1234   → your invoice
GET /api/invoice/1235   → another user's invoice (IDOR!)

PUT /api/profile/1234   → update your profile
PUT /api/profile/1235   → update another user's profile (IDOR!)

The server trusts the ID without verifying ownership.

Finding IDOR

1. Look for IDs in:
   - URL parameters: /invoice/1234
   - JSON body: {"id": 1234, "action": "delete"}
   - Hidden form fields: <input type="hidden" name="user_id" value="42">
   - Headers: X-User-ID, X-Resource-ID
   - Cookies: resource_id=1234

2. Test increment/decrement:
   id=1234  id=1233, id=1235

3. Test other users' IDs (use two accounts):
   Account A: get your invoice_id=100
   Account B: try GET /invoice/100

4. Try GUIDs:
   UUIDs look random but may be predictable or guessable
   GUID IDOR: get another user's GUID from email preview, invitation link, shared content

5. Try API endpoints directly:
   /api/admin/users  may not enforce auth
   /api/user/delete/1  no auth check

IDOR Variants

Horizontal IDOR: Access other users' same-privilege resources
   User A reads User B's private messages

Vertical IDOR: Access higher-privilege resources
   Regular user accesses admin endpoints

Indirect IDOR: Change parameter that references an object
   filename=report.pdf → filename=admin_report.pdf
   orderRef=abc123 → orderRef=admin_audit_log

Business Logic Vulnerabilities

Price/Quantity Manipulation

# Vulnerable checkout:
# Server trusts client-supplied price:
price = request.POST.get("price")   # attacker sets price=0.01
Process_payment(price)

# Or: negative quantities for refunds:
quantity=-1   # checkout gives you a refund!

# Or: integer overflow:
quantity=99999999999   # overflows to negative in some systems

# Fix: always calculate price server-side from product ID:
product = Product.objects.get(id=product_id)
total = product.price * quantity  # not from user input

Workflow Step Bypass

Registration flow: /step1  /step2  /step3/confirm
Normal flow enforced by state on server? If not:

Skip to: POST /step3/confirm with crafted body
 Complete registration without doing required steps

Example: email verification bypass
1. POST /register  account created, verify-required
2. POST /verify_email  sends email
3. GET /dashboard  blocked (not verified)
4. Try: POST /api/account/upgrade (skipping verification state check)
 May succeed if step isn't checked per-action

Coupon/Discount Abuse

1. Apply same coupon multiple times (no server-side use tracking)
2. Apply discount to non-eligible products
3. Combine non-combinable coupons
4. Race condition: two simultaneous /apply_coupon requests
    both read "not used" before either writes "used"
    both apply!

Fix:
- Database-level uniqueness constraint on coupon use
- Atomic transactions with SELECT FOR UPDATE

Mass Assignment

# Vulnerable Django view — updates ALL fields from POST data:
user = User.objects.get(id=user_id)
for key, value in request.POST.items():
    setattr(user, key, value)
user.save()

# Attacker POST: is_admin=True, role=admin
# → Privilege escalation!

# Fix — allowlist only editable fields:
ALLOWED_FIELDS = {"name", "email", "bio"}
for key, value in request.POST.items():
    if key in ALLOWED_FIELDS:
        setattr(user, key, value)

# Django REST Framework: read_only_fields or explicit fields in serializer
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ["name", "email"]  # explicit allowlist
        read_only_fields = ["is_admin", "role"]

Race Conditions in Business Logic

# Vulnerable: two requests simultaneously check balance:
def transfer(amount, from_user, to_user):
    balance = from_user.balance  # reads 1000
    if balance < amount:
        raise ValueError("Insufficient")
    from_user.balance -= amount   # both threads subtract!
    from_user.save()
    to_user.balance += amount
    to_user.save()

# Race condition: two simultaneous $800 transfers from $1000 balance
# Thread 1: reads 1000, subtracts 800, saves 200
# Thread 2: reads 1000 (before Thread 1 saves!), subtracts 800, saves 200
# Result: two $800 transfers from $1000 balance!

# Fix: database-level atomic update with locking
from django.db import transaction

with transaction.atomic():
    user = User.objects.select_for_update().get(id=from_user_id)
    if user.balance < amount:
        raise ValueError("Insufficient funds")
    user.balance -= amount
    user.save()

Authorization Checks

Broken Function Level Access Control

# Vulnerable: only UI hides admin buttons — no server check
@app.route("/admin/delete_user/<int:user_id>")
def admin_delete_user(user_id):
    # No auth check!
    User.objects.get(id=user_id).delete()
    return "Deleted"

# Fix: check on EVERY function
from django.contrib.auth.decorators import login_required, user_passes_test

@login_required
@user_passes_test(lambda u: u.is_staff)
def admin_delete_user(request, user_id):
    user = User.objects.get(id=user_id)
    # Also check: is request.user trying to delete themselves?
    if user == request.user:
        raise PermissionDenied
    user.delete()

IDOR-Safe Object Access Pattern

# Always filter by owner:
def get_invoice(request, invoice_id):
    # WRONG: Lesson.objects.get(id=invoice_id)
    # CORRECT: filter by owner
    try:
        invoice = Invoice.objects.get(id=invoice_id, owner=request.user)
    except Invoice.DoesNotExist:
        raise Http404  # don't reveal existence
    return invoice

# For admin access: check role separately
def get_any_invoice(request, invoice_id):
    if not request.user.is_staff:
        raise PermissionDenied
    return Invoice.objects.get(id=invoice_id)

On a web app with /api/orders/{id}: (1) find your order ID (e.g., 1042), (2) try adjacent IDs: 1041, 1043 — do you get other users' orders? (3) try /api/admin/orders without admin role, (4) look for GUIDs in API responses — try one from a different user's email, (5) document each IDOR finding with impact (what data is exposed) and the fix (filter by owner).

✦ Answer the questions to complete this task

Why do GUIDs not fix IDOR vulnerabilities?

On a shopping cart endpoint: (1) add item to cart, intercept checkout POST, change price=100 to price=0.01 — does server accept it? (2) try quantity=-1 for negative total, (3) apply same coupon twice, (4) add item, start checkout, change product_id to a more expensive product while keeping price for the cheap one, (5) fix: server must calculate price from DB using product_id, never from client.

✦ Answer the questions to complete this task

What is a race condition in coupon redemption?

On a profile update endpoint (PUT /api/profile): (1) send normal update: {name: 'Alice'}, (2) add is_admin: true — check if it's accepted, (3) try role: 'admin', (4) try balance: 999999, (5) fix the Django serializer to use explicit fields allowlist and read_only_fields for is_admin and role.

✦ Answer the questions to complete this task

What Django REST Framework feature prevents mass assignment?

💪 Exercises & Challenges

📝 MCQ Medium +20 XP

Business Logic & IDOR MCQ

Business Logic & IDOR MCQ

Start →
⚙️ Practical Medium +30 XP

IDOR and Business Logic Audit

IDOR and Business Logic Audit

Start →
🚩 Challenge Hard +50 XP

IDOR to Account Takeover

IDOR to Account Takeover

Start →