Skip to content
Sedat Özdemir
Writing

api-security

Leaving the API Doors Unlocked: Did You Really Think a JWT Was Enough?

Have you ever felt that cold sweat down your neck when you realize your API is serving your entire database to the internet? Let's talk about the most common 'invisible' vulnerability: BOLA.

Sedat Özdemir
· 4 dk read

Have you ever felt that cold sweat trickling down your neck the moment you realize the API you wrote is serving your entire database to the internet without you even noticing? I’m talking about that famous sense of security where we say, 'Man, we checked everything, there's token validation, no one can get in without logging in.' That feeling usually shatters into a thousand pieces the second a Red Team member changes a single parameter in your API and reaches the 100,000th record.

Today, over a cup of coffee, we're going to talk about the most insidious, 'hidden in plain sight' plague of the modern web world: Broken Object Level Authorization (BOLA), or as our old friend used to call it, IDOR. But we won't be dealing with dry theoretical definitions; we'll talk about what actually happens in the field and the logic errors that drive us crazy during tests.

Locking the Door Isn't Enough; You Must Check the Safe Inside

We often fall into this trap: 'Did the user log in? Yes. Do they have a valid JWT (JSON Web Token)? Yes. Okay, then we can respond to this request.' This is the biggest victim of trying to manage security with a 'check-box' mentality.

Let’s imagine a scenario. You’re working at a fintech startup called testCompany. You have an endpoint where users view their invoices.

Defanged Request Example:

GET /api/v1/billing/invoice/10045 HTTP/1.1
Host: api.example.com
Authorization: Bearer <valid_jwt_of_user_A>

Here, User 'A' sees their own invoice (ID: 10045). Everything looks perfectly legal. But what happens if this user gets a bit curious and changes that '10045' to '10046' via Burp Suite or even just in the browser address bar?

If your backend code only checks 'Is this user logged in?' but doesn't ask 'Does this user actually own invoice number 10046?', then it's game over. You are now watching a rival company's invoice, your neighbor's expenses, or the financial data of the entire system.

Why Do We Still Make These Mistakes?

Look, friend, let's be honest; things move fast. Sprints need to be finished, features need to be released, and the product manager is breathing down your neck. My developer friends usually write the database query like this:

# Bad Practice (Pseudo-code)
def get_invoice(invoice_id):
    # User login check is done here (Middleware)
    invoice = db.query("SELECT * FROM invoices WHERE id = ?", invoice_id)
    return invoice

The problem here is that invoice_id is a primary key, and the query doesn't verify ownership. As a Red Teamer, we call this 'Horizontal Privilege Escalation.' In other words, users at the same authorization level accessing each other's data.

From the Attacker's Perspective: Why Are Those Numbers So Sequential?

When we try to breach a system, one of the first things we look at is the ID structure. If IDs are increasing like 1, 2, 3... (Incremental IDs), that’s an open buffet for us.

We could write a simple script (please don't try this at home, or only test it in your own 127.0.0.1 environment) to send thousands of requests in seconds:

# Defanged/Harmless Example
for id in {10000..10100}; do 
  curl -H "Authorization: Bearer <valid_token>" "http://127.0.0.1/api/v1/billing/invoice/$id"; 
done

To prevent this, the fix is simple but critical: Always validate the object owner in every query and consider using non-predictable UUIDs instead of sequential IDs. Stay safe, and remember: authentication is knowing who the user is; authorization is knowing what they are allowed to touch.

Related posts