Verified by Garnet Grid

API Security Hardening: OWASP Top 10 Implementation

Secure your APIs against the OWASP API Security Top 10. Covers authentication, authorization, rate limiting, input validation, and security testing with practical code examples.

APIs are the #1 attack surface in modern applications. Every mobile app, SPA, microservice, and third-party integration communicates through APIs. The OWASP API Security Top 10 identifies the most critical risks based on real-world breach data. This guide translates each risk into actionable security controls with production-ready implementation code.

The distinction between “application security” and “API security” is important: APIs expose raw business logic and data, often with less protection than web UIs. A web form might show you 10 records; the underlying API returns 10,000. A web form validates input client-side; the API must validate server-side because any client can call it directly.


OWASP API Security Top 10

#RiskSeverityReal-World Impact
1Broken Object Level Authorization (BOLA)CriticalUser A accesses User B’s data by changing an ID in the URL
2Broken AuthenticationCriticalWeak tokens, no MFA, credential stuffing
3Broken Object Property Level AuthorizationHighMass assignment — user sets admin=true in request body
4Unrestricted Resource ConsumptionMediumDoS via expensive queries, no pagination limits
5Broken Function Level AuthorizationHighRegular user accesses admin endpoints
6Unrestricted Access to Sensitive FlowsMediumBot abuse of checkout, registration, or password reset
7Server Side Request Forgery (SSRF)HighAPI fetches internal URLs, exposing cloud metadata
8Security MisconfigurationMediumVerbose errors, default credentials, CORS misconfigured
9Improper Inventory ManagementLowShadow APIs, deprecated endpoints still live
10Unsafe Consumption of APIsMediumBlindly trusting third-party API responses

#1: Broken Object Level Authorization (BOLA)

The single most common API vulnerability. Every endpoint that accepts a resource identifier (ID, slug, UUID) must verify that the requesting user has permission to access that specific resource.

# ❌ VULNERABLE: No ownership check — anyone can access any order
@app.get("/api/orders/{order_id}")
def get_order(order_id: str):
    return db.get_order(order_id)  # Missing authorization check

# ✅ SECURE: Verify ownership before returning data
@app.get("/api/orders/{order_id}")
def get_order(order_id: str, current_user: User = Depends(get_current_user)):
    order = db.get_order(order_id)
    if order is None:
        raise HTTPException(404)
    if order.user_id != current_user.id and not current_user.is_admin:
        raise HTTPException(403, "Access denied")
    return order

BOLA Prevention Patterns

PatternWhen to UseImplementation
Direct ownership checkUser-owned resources (orders, profiles)resource.user_id == current_user.id
Team/org membershipTeam resources (projects, dashboards)Check user’s team membership in DB
RBAC with resource scopeComplex permissionsRole + resource-level permission check
UUIDs instead of sequential IDsAll resourcesPrevents ID enumeration attacks

#2: Broken Authentication

Authentication is the front door of your API. Weak authentication means everything behind it is exposed.

# Secure JWT implementation with short-lived tokens and revocation
from jose import jwt
from datetime import datetime, timedelta

SECRET_KEY = os.environ["JWT_SECRET"]  # Never hardcode
ALGORITHM = "HS256"

def create_access_token(user_id: str, roles: list, expires_minutes: int = 15) -> str:
    """Short-lived access token — 15 minutes default"""
    payload = {
        "sub": user_id,
        "roles": roles,
        "exp": datetime.utcnow() + timedelta(minutes=expires_minutes),
        "iat": datetime.utcnow(),
        "jti": str(uuid.uuid4()),  # Unique token ID for revocation
    }
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

def create_refresh_token(user_id: str, expires_days: int = 7) -> str:
    """Longer-lived refresh token — stored securely, used to get new access tokens"""
    payload = {
        "sub": user_id,
        "type": "refresh",
        "exp": datetime.utcnow() + timedelta(days=expires_days),
        "jti": str(uuid.uuid4()),
    }
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

def verify_token(token: str) -> dict:
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        if is_token_revoked(payload["jti"]):
            raise HTTPException(401, "Token revoked")
        return payload
    except jwt.ExpiredSignatureError:
        raise HTTPException(401, "Token expired")
    except jwt.JWTError:
        raise HTTPException(401, "Invalid token")

Authentication Checklist

ControlStatusNotes
Access token expiry ≤ 15 minRequiredLimits exposure window
Refresh token with secure storageRequiredHttpOnly cookie or secure storage
Token revocation capabilityRequiredBlocklist by JTI on logout/password change
Brute force protectionRequiredRate limiting on auth endpoints (see below)
Password hashing (bcrypt/argon2)RequiredNever store plaintext or MD5/SHA1
MFA for sensitive operationsRecommendedPayment, admin, profile changes

#4: Rate Limiting

Without rate limiting, attackers can brute-force credentials, scrape data, and cause denial-of-service. Different endpoints need different limits.

from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

@app.post("/api/login")
@limiter.limit("5/minute")  # Brute force prevention — very strict
def login(request: Request, credentials: LoginRequest):
    return authenticate(credentials)

@app.get("/api/search")
@limiter.limit("30/minute")  # Search abuse prevention — moderate
def search(request: Request, q: str):
    return perform_search(q)

@app.post("/api/orders")
@limiter.limit("10/minute")  # Transaction rate limiting — business logic
def create_order(request: Request, order: OrderRequest):
    return process_order(order)

@app.get("/api/products")
@limiter.limit("100/minute")  # Read endpoints — more permissive
def list_products(request: Request):
    return get_products()

Rate Limiting Strategy

Endpoint TypeRecommended LimitReason
Authentication (login, register, reset)5-10/minute per IPBrute force prevention
Write operations (create, update, delete)10-30/minute per userAbuse prevention
Search / query endpoints30-60/minute per userScraping prevention
Read operations (list, get)100-300/minute per userAllow normal usage
File upload5/minute per userResource consumption control
Webhook endpoints100-500/minute per sourceBurst handling

Input Validation

Never trust client input. Validate every field with strict schemas and sanitize before processing.

from pydantic import BaseModel, validator, constr, conint
import re

class CreateUserRequest(BaseModel):
    email: constr(max_length=254)
    name: constr(min_length=1, max_length=100)
    phone: constr(max_length=20) | None = None
    age: conint(ge=13, le=150) | None = None  # Reasonable bounds

    @validator('email')
    def validate_email(cls, v):
        pattern = r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
        if not re.match(pattern, v):
            raise ValueError('Invalid email format')
        return v.lower().strip()

    @validator('name')
    def sanitize_name(cls, v):
        # Remove potential XSS characters
        return re.sub(r'[<>"\'\/;]', '', v).strip()

class PaginationParams(BaseModel):
    """Prevent unbounded queries"""
    page: conint(ge=1, le=10000) = 1
    page_size: conint(ge=1, le=100) = 20  # Max 100 items per page
    sort_by: str = "created_at"

    @validator('sort_by')
    def validate_sort_field(cls, v):
        allowed = {"created_at", "updated_at", "name", "price"}
        if v not in allowed:
            raise ValueError(f'Sort field must be one of: {allowed}')
        return v

Security Headers

from fastapi.middleware.cors import CORSMiddleware

# CORS — restrict to known origins only
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.garnetgrid.com"],  # Never use "*" in production
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["Authorization", "Content-Type"],
    allow_credentials=True,
)

# Security headers middleware
@app.middleware("http")
async def add_security_headers(request, call_next):
    response = await call_next(request)
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "DENY"
    response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
    response.headers["X-XSS-Protection"] = "1; mode=block"
    response.headers["Cache-Control"] = "no-store"
    response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
    response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()"
    return response

API Security Testing

Automated Scanning in CI/CD

# OWASP ZAP automated scan against OpenAPI spec
docker run -t owasp/zap2docker-stable zap-api-scan.py \
  -t https://api.garnetgrid.com/openapi.json \
  -f openapi \
  -r security-report.html

# Nuclei — community-driven vulnerability scanner
nuclei -u https://api.garnetgrid.com \
  -t http/vulnerabilities/ \
  -severity critical,high

# Dredd — API contract testing against OpenAPI spec
dredd openapi.yaml https://api.garnetgrid.com

Manual Testing Checklist

TestHowWhat to Check
BOLAChange resource IDs in requestsAccess to other users’ resources
Auth bypassRemove/modify Authorization headerEndpoint still returns data
Mass assignmentAdd extra fields to POST/PUT bodyAdmin role, price, or status changes
SQL injectionSend ' OR 1=1 -- in parametersError messages, data leakage
Rate limit bypassSend rapid requests from multiple IPsLimits actually enforced
SSRFSubmit internal URLs in user-controlled fieldsAccess to cloud metadata (169.254.169.254)

API Versioning for Security

Never deprecate without decommissioning. Old API versions are a common attack vector because they often lack the security improvements of newer versions.

PracticeImplementation
Version in URL/api/v1/users, /api/v2/users
Sunset headerSunset: Sat, 01 Jan 2026 00:00:00 GMT
Deprecation noticeAPI response header: Deprecation: true
Force upgradeReturn 410 Gone after sunset date
InventoryMaintain living API catalog (Swagger, Backstage)

Checklist

  • BOLA checks on every endpoint that accepts resource IDs (no exceptions)
  • JWT tokens with short expiry (≤ 15 min) + secure refresh tokens
  • Rate limiting tuned per endpoint type (auth, write, read, search)
  • Input validation with strict schemas (Pydantic, Joi, Zod)
  • CORS restricted to known origins (never wildcard in production)
  • Security headers on all responses (HSTS, CSP, X-Frame-Options)
  • API keys rotatable and scoped to minimum permissions
  • Automated security scanning in CI/CD (ZAP, Nuclei, Trivy)
  • Logging of all auth failures and suspicious activity
  • API inventory maintained — no shadow or deprecated endpoints live
  • Error messages sanitized (no stack traces, no internal paths)

:::note[Source] This guide is derived from operational intelligence at Garnet Grid Consulting. For API security consulting, visit garnetgrid.com. :::

Jakub Dimitri Rezayev
Jakub Dimitri Rezayev
Founder & Chief Architect • Garnet Grid Consulting

Jakub holds an M.S. in Customer Intelligence & Analytics and a B.S. in Finance & Computer Science from Pace University. With deep expertise spanning D365 F&O, Azure, Power BI, and AI/ML systems, he architects enterprise solutions that bridge legacy systems and modern technology — and has led multi-million dollar ERP implementations for Fortune 500 supply chains.

View Full Profile →