OWASP API Security Top 10: Real Attack Examples and Code-Level Defenses

APIs (Application Programming Interfaces) power the modern web. Every app on your phone is talking to APIs. Every SaaS product exposes APIs. And every API is a potential attack surface. API security vulnerabilities are responsible for billions of dollars in breaches — including the 2022 Optus breach that exposed 11 million Australian customers and the 2021 LinkedIn scraping incident that exposed 700 million profiles. This guide covers the OWASP API Security Top 10 with real attack examples and code-level defenses.

OWASP API Security Top 10

API1: Broken Object Level Authorization (BOLA)

BOLA is the most common API vulnerability. The API doesn’t verify that the requesting user has the right to access the specific object they’re requesting. An attacker simply changes an ID in the URL to access another user’s data.

# VULNERABLE: This API returns any user's data if you know the ID
GET /api/v1/users/1234/profile
Authorization: Bearer USER_A_TOKEN
# Returns: User 1234's complete profile, even if you are User 5678

# The attack: simply change the user ID
GET /api/v1/users/1235/profile  # User B's profile
GET /api/v1/users/1236/profile  # User C's profile

# Real-world example: The 2022 Optus breach
# GET /api/v2/customers/{customer_id}
# The API required authentication but didn't verify the token matched the requested customer_id
# Attacker iterated through customer_ids: 1000000 to 11000000
# Result: 11 million customer records (name, DOB, address, passport, driver's license) exposed

# Testing for BOLA with Burp Suite:
# 1. Log in as User A, make a request, note your user ID in the response
# 2. Log in as User B, use User A's token with User B's ID
# 3. If you get User A's data with User B's token = BOLA vulnerability

# SECURE implementation (Python/Flask example)
from flask import request, jsonify, abort
from functools import wraps

def authorize_user_access(f):
    @wraps(f)
    def decorated_function(user_id, *args, **kwargs):
        current_user = get_current_user_from_token(request.headers.get('Authorization'))
        if current_user.id != user_id and not current_user.is_admin:
            abort(403)  # Forbidden
        return f(user_id, *args, **kwargs)
    return decorated_function

@app.route('/api/v1/users/<int:user_id>/profile')
@authenticate
@authorize_user_access  # Verify token matches requested resource
def get_user_profile(user_id):
    return jsonify(User.query.get_or_404(user_id).to_dict())

API2: Broken Authentication

# Common authentication failures:

# 1. Weak JWT secret key
# JWT tokens are signed with a secret key
# If the secret is weak, it can be brute-forced
jwt-cracker eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4ifQ.SIGNATURE
# hashcat -m 16500 jwt_token.txt wordlist.txt

# 2. JWT algorithm confusion (alg:none attack)
# Change algorithm to "none" and remove signature
# Original: {"alg":"HS256","typ":"JWT"}
# Attacker changes to: {"alg":"none","typ":"JWT"}
# Some vulnerable libraries accept this unsigned token

# 3. Brute force: No rate limiting on login endpoint
for i in range(1000):
    response = requests.post('/api/v1/login', json={'username': 'admin', 'password': passwords[i]})
    if response.status_code == 200:
        print(f"Password found: {passwords[i]}")

# SECURE: Proper JWT validation
import jwt
from datetime import datetime, timedelta

def create_token(user_id):
    payload = {
        'sub': user_id,
        'iat': datetime.utcnow(),
        'exp': datetime.utcnow() + timedelta(hours=1),  # Short expiry
        'jti': str(uuid.uuid4())  # Unique token ID (for revocation)
    }
    return jwt.encode(payload, SECRET_KEY, algorithm='HS256')  # Strong secret: 256+ bit random

def verify_token(token):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])  # Whitelist algorithms
        return payload
    except jwt.ExpiredSignatureError:
        return None
    except jwt.InvalidTokenError:
        return None

API3: Broken Object Property Level Authorization

# API returns more data than the caller should see (over-exposure)
# Or allows modification of properties the caller shouldn't control

# VULNERABLE: Mass assignment
# Attacker sends extra fields to elevate privileges
PUT /api/v1/users/5678/profile
{
  "name": "John Doe",
  "email": "john@example.com",
  "is_admin": true,        # Attacker adds this!
  "credit_balance": 99999  # And this!
}
# Vulnerable ORM: user.update(request.json)  - updates ALL fields including is_admin

# SECURE: Explicit field allowlist
ALLOWED_UPDATE_FIELDS = {'name', 'email', 'phone', 'address'}

@app.route('/api/v1/users/<int:user_id>/profile', methods=['PUT'])
@authenticate
def update_profile(user_id):
    data = request.json
    # Only update allowed fields
    safe_data = {k: v for k, v in data.items() if k in ALLOWED_UPDATE_FIELDS}
    user = User.query.get_or_404(user_id)
    for key, value in safe_data.items():
        setattr(user, key, value)
    db.session.commit()
    return jsonify(user.to_dict())

API5: Broken Function Level Authorization

# Admin endpoints that regular users shouldn't access
# Attackers guess/enumerate admin endpoints

# Common patterns to test:
GET /api/v1/admin/users
GET /api/v2/management/settings
POST /api/internal/create_admin
DELETE /api/v1/users/all

# Test with a regular user token - should return 403, not data

# Real-world: Many APIs expose admin endpoints without checking role
# Just adding /admin/ to the path or /v2/ (newer version with admin features)

API8: Security Misconfiguration

# Common misconfigurations:

# 1. Exposed debug endpoints in production
GET /api/v1/debug/env      # Returns environment variables!
GET /api/v1/actuator/env   # Spring Boot actuator (exposes server internals)
GET /api/v1/swagger-ui     # API documentation with all endpoints listed

# 2. Missing security headers
curl -I https://api.example.com
# Missing: X-Content-Type-Options, X-Frame-Options, HSTS, CORS misconfiguration

# 3. Open CORS policy (allows any origin to make cross-site requests)
# BAD:
Access-Control-Allow-Origin: *
# GOOD:
Access-Control-Allow-Origin: https://yourdomain.com

# Fix: Add security headers in Flask/Express
from flask import Flask
from flask_security_headers import SecurityHeaders

app = Flask(__name__)
sh = SecurityHeaders()
sh.init_app(app, {
    'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
    'X-Content-Type-Options': 'nosniff',
    'X-Frame-Options': 'DENY',
    'Content-Security-Policy': "default-src 'self'"
})

API Security Testing Tools

# 1. Burp Suite: Manual API testing
# Import API spec (OpenAPI/Swagger) to automatically crawl all endpoints
# Use Intruder to test BOLA by fuzzing IDs

# 2. OWASP ZAP: Automated API scanning
docker run -v $(pwd):/zap/wrk/:rw -t owasp/zap2docker-stable   zap-api-scan.py -t https://api.example.com/openapi.json -f openapi

# 3. Postman: API testing and automation
# Create collection with all API endpoints
# Add security tests as pre-request scripts:
pm.test("No sensitive data in response", function () {
    const response = pm.response.json();
    pm.expect(JSON.stringify(response)).to.not.include("password");
    pm.expect(JSON.stringify(response)).to.not.include("secret");
});

# 4. JWT.io / jwt-toolkit: JWT analysis
# Install jwt-tool
pip3 install jwt-tool
jwt_tool eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.SIGNATURE -T  # Tamper mode
jwt_tool TOKEN -C -d wordlist.txt  # Crack JWT secret

# 5. ffuf: API endpoint fuzzing
ffuf -u https://api.target.com/api/FUZZ -w api-wordlist.txt -mc 200,201,403
# api-wordlist.txt: https://wordlists.assetnote.io/

API Rate Limiting with NGINX

# /etc/nginx/nginx.conf
http {
    # Rate limiting zones
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;  # 10 req/sec per IP
    limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;  # 5 login attempts per min

    server {
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            limit_req_status 429;
        }

        location /api/v1/login {
            limit_req zone=login;
            limit_req_status 429;
        }
    }
}

# Add retry-after header when rate limited
add_header Retry-After 60;  # Tell client to wait 60 seconds

API security starts with the OWASP API Security Top 10 but doesn’t end there. The most important practice is implementing authentication and authorization checks at every endpoint, every time — never trusting client-supplied IDs without verification, and never returning more data than the caller specifically needs. APIs built with security as an afterthought become headlines. APIs built with security in mind from day one remain tools.