9 min read vulnerability-analysis

Deep Dive into the OWASP API Security Top 10 Standard with Practical Examples

A comprehensive technical analysis of the industry-standard OWASP API Security framework. Explore critical API vulnerabilities including BOLA (API1), BOPLA (API3), and Unrestricted Resource Consumption (API4) with Python examples and secure fixes.

Deep Dive into the OWASP API Security Top 10 Standard with Practical Examples
M

Marcus Vance, OSCE

Principal Exploit Developer & Red Team Lead

Marcus Vance is a seasoned red teamer, vulnerability researcher, and offensive security professional. He writes about advanced SQL injection bypasses, insecure deserialization mechanics, custom WAF evasion, and binary reverse engineering.

The integration of microservices, mobile apps, and Single Page Applications (SPAs) has shifted the focus of modern web development entirely to APIs. Today, APIs serve more than 80% of all public web traffic. However, traditional Web Application Firewalls (WAFs) are often blind to API logic attacks, as a malicious actor’s payload format looks perfectly valid under standard HTTP parsing rules.

The OWASP API Security Top 10 remains the definitive global standard for cataloging and mitigating specific API vulnerabilities. Even as backend architectures evolve with high-performance cloud databases, GraphQL resolvers, and decentralized service meshes, the fundamental security vectors outlined in this framework remain critical. In this article, we will dissect three of the most common and dangerous vulnerability categories: BOLA (API1), BOPLA (API3), and Unrestricted Resource Consumption (API4). We will look at vulnerable Python implementations and demonstrate how to write clean, secure code to patch them.


1. API1: Broken Object Level Authorization (BOLA)

BOLA (formerly IDOR — Insecure Direct Object Reference) remains the leading threat in API security. This vulnerability occurs when an API endpoint accepts an object identifier (such as /api/v1/billing/10025) and returns the resource payload without validating if the requested object belongs to the currently authenticated user. In modern environments, BOLA frequently targets deep nesting in GraphQL resolvers and microservice internal IPC boundaries.

Vulnerable Code Example (Python / FastAPI):

from fastapi import FastAPI, Depends, HTTPException, status
from pydantic import BaseModel

app = FastAPI()

# Simulated billing database
billings_db = {
    10025: {"id": 10025, "user_id": "alice_uuid", "amount": 250.00, "description": "Cloud Servers"},
    10026: {"id": 10026, "user_id": "bob_uuid", "amount": 1200.00, "description": "Enterprise License"}
}

# Stub dependency to retrieve current user from JWT claims
def get_current_user():
    # In production, JWT is verified and decoded here
    return {"uuid": "alice_uuid", "username": "Alice"}

@app.get("/api/v1/billing/{billing_id}")
def get_billing(billing_id: int, current_user: dict = Depends(get_current_user)):
    billing = billings_db.get(billing_id)
    if not billing:
        raise HTTPException(status_code=404, detail="Billing record not found")
    
    # VULNERABILITY: We verified the user is authenticated (valid token),
    # but completely forgot to check if the record billing_id actually belongs to them!
    return billing

Attack Scenario:

An attacker (Bob) logs into the system and sees his own billing invoice endpoint /api/v1/billing/10026. He opens Burp Suite or writes a basic Python crawler and begins iterating the ID backwards. He sends the following request:

GET /api/v1/billing/10025 HTTP/1.1
Host: vulnerable-api.com
Authorization: Bearer <bob_jwt_token>

The server returns Alice’s private financial data. The API returned the object purely based on the fact that Bob’s token is cryptographic and valid, with no ownership checks.

Secure Remediation:

Always check resource ownership explicitly inside your business logic or directly in your database query predicates:

@app.get("/api/v1/billing/{billing_id}")
def get_billing_secure(billing_id: int, current_user: dict = Depends(get_current_user)):
    billing = billings_db.get(billing_id)
    if not billing:
        raise HTTPException(status_code=404, detail="Billing record not found")
    
    # FIX: Verify resource association with current user
    if billing["user_id"] != current_user["uuid"]:
         raise HTTPException(
             status_code=status.HTTP_403_FORBIDDEN, 
             detail="You do not have permission to access this resource"
         )
         
    return billing

Recommendation: Use non-sequential, random identifiers (UUIDv4) instead of incremental integers. While this mitigates ID guessing, it does not replace the requirement for strict, server-side authorization checks.


2. API3: Broken Object Property Level Authorization (BOPLA)

BOPLA consolidated two legacy categories: Excessive Data Exposure (returning unnecessary properties in API responses) and Mass Assignment (blindly updating object attributes from client payloads). The risk occurs when APIs expose sensitive database columns or allow users to mutate unauthorized properties directly from request payloads (such as modifying is_admin, credit_balance, or internal database updated_at properties).

Vulnerable Code Example (Mass Assignment):

class UserUpdateSchema(BaseModel):
    # Developer reused the base user model for profile updates,
    # leaving administrative attributes exposed
    username: str | None = None
    email: str | None = None
    is_admin: bool | None = False  # THIS FIELD SHOULD ONLY BE MODIFIED BY AN ADMIN!

@app.put("/api/v1/user/profile")
def update_profile(data: UserUpdateSchema, current_user: dict = Depends(get_current_user)):
    user_id = current_user["uuid"]
    
    # Simulated db lookup
    db_user = get_user_from_db(user_id)
    
    # VULNERABILITY: Blindly iterating incoming JSON and mapping directly to the DB object
    for field, value in data.dict(exclude_unset=True).items():
        db_user[field] = value
        
    save_user_to_db(db_user)
    return {"status": "profile updated", "user": db_user}

Attack Scenario:

An attacker sends a PUT request with a modified JSON body, adding a hidden field discovered via Swagger API documentation (openapi.json):

PUT /api/v1/user/profile HTTP/1.1
Host: vulnerable-api.com
Content-Type: application/json
Authorization: Bearer <attacker_token>

{
  "username": "hacker1337",
  "is_admin": true
}

Because the backend maps all incoming dictionary keys directly to the database user record, the attacker successfully elevates their privileges to system administrator (Privilege Escalation).

Secure Remediation:

Strictly isolate incoming and outgoing data structures using Data Transfer Objects (DTOs). Never use the same data schema for reading, creation, and profile updating.

class UserProfileUpdateInput(BaseModel):
    # Only expose fields that a standard user is permitted to self-manage!
    username: str | None = None
    email: str | None = None

@app.put("/api/v1/user/profile")
def update_profile_secure(data: UserProfileUpdateInput, current_user: dict = Depends(get_current_user)):
    user_id = current_user["uuid"]
    db_user = get_user_from_db(user_id)
    
    # Apply only the permitted input attributes
    update_data = data.dict(exclude_unset=True)
    for field in update_data:
        db_user[field] = update_data[field]
        
    save_user_to_db(db_user)
    return {"status": "success"}

3. API4: Unrestricted Resource Consumption

This category focuses on failures to restrict system consumption footprint, particularly in high-throughput cloud environments. This category targets missing rate limiters, unbounded files payload allocation, resource depletion via heavy pagination queries, and unbounded CPU-bound cryptography parsing.

Vulnerable Pagination Example (Database Resource Exhaustion):

If an API accepts user-controlled limits for pagination queries without verifying ceiling boundaries:

@app.get("/api/v1/logs")
def get_logs(limit: int = 20, offset: int = 0):
    # VULNERABILITY: If an attacker passes limit=10000000,
    # the database engine will attempt to allocate millions of rows in memory,
    # triggering Out-Of-Memory (OOM) crashes and Denial of Service (DoS)
    return db.query("SELECT * FROM logs LIMIT %s OFFSET %s", (limit, offset))

Secure Remediation:

Enforce hard constraints on user-input values during query validation:

@app.get("/api/v1/logs")
def get_logs_secure(limit: int = 20, offset: int = 0):
    # Enforce a strict ceiling limit
    MAX_LIMIT = 100
    if limit > MAX_LIMIT:
        limit = MAX_LIMIT
        
    return db.query("SELECT * FROM logs LIMIT %s OFFSET %s", (limit, offset))

Additionally, implement Rate Limiting at the reverse proxy or API gateway layer (e.g., OpenResty, Kong, Traefik) or directly via Redis tokens in the backend:

Limit: 100 requests per minute per IP / API token

Conclusion

API security is not a single firewall configuration task; it requires integrating secure software design principles directly into application architecture. Isolating database schemas from request payloads, authorizing every transaction against resource ownership, and enforcing strict limits on incoming payloads constitute the bare minimum foundation required for modern web applications to survive in production environments.

Read more tech interview & security careers guides on the PentestLab blog.