API Security

Comprehensive guide to securing your APIs against modern threats. Learn authentication, authorization, common vulnerability patterns, OWASP API Security Top 10, and best practices for building resilient, secure APIs.

Introduction

API Security encompasses the strategies, practices, and tools used to protect Application Programming Interfaces (APIs) from malicious attacks and unauthorized access. As the backbone of modern software architecture, APIs are the primary communication layer between services, making them a critical attack surface.

Today, APIs power virtually everything: mobile applications, single-page applications (SPAs), IoT devices, microservices, third-party integrations, and cloud infrastructure. A single compromised API endpoint can expose millions of records, bypass authentication mechanisms, or allow full system takeover.

Common API Types

  • REST (Representational State Transfer) - The most common architectural style for APIs, using HTTP methods (GET, POST, PUT, DELETE) with JSON or XML payloads.
  • GraphQL - A query language for APIs developed by Facebook, allowing clients to request exactly the data they need. Powerful but introduces unique security challenges.
  • gRPC - A high-performance RPC framework by Google using Protocol Buffers and HTTP/2. Commonly used in microservice-to-microservice communication.
  • WebSocket - Provides full-duplex communication channels over a single TCP connection, commonly used for real-time features like chat and live feeds.
Key Concept

API security is not just about authentication. It covers the entire lifecycle: design, implementation, deployment, monitoring, and deprecation. A secure API must enforce authentication, authorization, input validation, rate limiting, and proper error handling at every endpoint.

Why API Security

APIs are now the most targeted attack vector in modern applications. According to industry reports, over 90% of web applications have experienced an API security incident. The consequences include data breaches, financial loss, regulatory penalties, and reputational damage.

The Attack Surface

Every API endpoint is a potential entry point for attackers. Unlike traditional web applications where the UI controls user interaction, APIs expose backend logic directly to consumers. This means:

  • Direct backend access - API consumers interact with business logic directly, bypassing client-side controls.
  • Multiple consumers - The same API may serve web apps, mobile apps, third-party integrations, and internal services, each with different trust levels.
  • Data exposure - APIs often return more data than the client needs, leading to over-exposure of sensitive information.
  • Automation risk - APIs are easily scriptable, making them susceptible to automated attacks at scale.
  • Complexity - Modern API architectures (microservices, serverless) create distributed attack surfaces that are harder to secure uniformly.
Warning

Over 90% of attacks in 2024 involved API endpoints, according to the Salt Security API Security Report. If your application exposes an API, it is already a target.

JWT Security

JSON Web Tokens (JWTs) are the most widely used token format for API authentication. A JWT consists of three Base64URL-encoded parts separated by dots:

jwt structure
// Header.Payload.Signature

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiIxMjM0NSIsInJvbGUiOiJ1c2VyIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

// Decoded Header:  { "alg": "HS256" }
// Decoded Payload: { "userId": "12345", "role": "user" }
// Signature: HMAC-SHA256(base64(header) + "." + base64(payload), secret)

Common JWT Vulnerabilities

1. "None" Algorithm Attack

If the server accepts the "none" algorithm, an attacker can remove the signature entirely and forge arbitrary tokens.

python - vulnerable
# VULNERABLE: Server accepts "none" algorithm
import jwt

# Attacker crafts a token with alg: "none"
header = {"alg": "none", "typ": "JWT"}
payload = {"userId": "12345", "role": "admin"}

# No signature required - token is accepted!
token = jwt.encode(payload, key="", algorithm="none")
python - secure
# SECURE: Explicitly specify allowed algorithms
import jwt

SECRET_KEY = "your-256-bit-secret-key-here"

# Decode with strict algorithm validation
try:
    payload = jwt.decode(
        token,
        SECRET_KEY,
        algorithms=["HS256"]  # Only allow HS256
    )
except jwt.InvalidSignatureError:
    print("Invalid token signature")
except jwt.ExpiredSignatureError:
    print("Token has expired")

2. Weak Secret Key

If the JWT signing secret is weak or short, attackers can brute-force it offline and forge tokens.

python - secure secret generation
import secrets

# Generate a cryptographically secure secret
SECRET_KEY = secrets.token_hex(32)  # 256-bit key

# NEVER use short or guessable secrets:
# "secret", "password123", "jwt-key" -- ALL VULNERABLE

3. No Expiration (Missing "exp" Claim)

Tokens without an expiration claim remain valid forever. If stolen, they can be used indefinitely.

javascript - secure with expiration
const jwt = require('jsonwebtoken');

const token = jwt.sign(
    { userId: '12345', role: 'user' },
    process.env.JWT_SECRET,
    {
        expiresIn: '1h',       // Token expires in 1 hour
        issuer: 'khmersec',    // Validate issuer
        audience: 'api',       // Validate audience
        algorithm: 'HS256'     // Explicit algorithm
    }
);

4. Stolen Tokens (No Revocation)

JWTs are stateless - once issued, they cannot be revoked until they expire. Use short expiration times and implement token revocation with a blocklist or refresh token rotation.

Critical

Never store JWTs in localStorage or sessionStorage. These are accessible via JavaScript and vulnerable to XSS attacks. Use HTTP-only, Secure, SameSite cookies instead.

OAuth 2.0

OAuth 2.0 is an authorization framework that allows third-party applications to obtain limited access to an HTTP service on behalf of a resource owner. It is the industry standard for delegated authorization.

Authorization Code Flow

The most common and secure OAuth 2.0 flow for server-side applications:

flow
# OAuth 2.0 Authorization Code Flow

1. Client redirects user to authorization server:
   GET /authorize?response_type=code&client_id=APP_ID&redirect_uri=CALLBACK&scope=read&state=RANDOM

2. User authenticates and grants consent

3. Authorization server redirects back with code:
   GET /callback?code=AUTH_CODE&state=RANDOM

4. Client exchanges code for tokens:
   POST /token
   Body: grant_type=authorization_code&code=AUTH_CODE&redirect_uri=CALLBACK&client_secret=SECRET

5. Server returns access_token + refresh_token
   { "access_token": "eyJhbG...", "refresh_token": "dGhpcyBp...", "expires_in": 3600 }

Common OAuth 2.0 Attacks

CSRF via Missing State Parameter

Without the state parameter, an attacker can initiate an OAuth flow with their own account and trick the victim into linking it.

python - vulnerable vs secure
# VULNERABLE: No state parameter
"/authorize?response_type=code&client_id=APP_ID&redirect_uri=CALLBACK"

# SECURE: Include and validate state parameter
import secrets

state = secrets.token_urlsafe(32)
session["oauth_state"] = state

auth_url = f"/authorize?response_type=code&client_id=APP_ID
            &redirect_uri=CALLBACK&state={state}"

# On callback, verify state matches
if request.args["state"] != session["oauth_state"]:
    raise SecurityError("OAuth state mismatch - possible CSRF")

Open Redirect

If the redirect_uri is not strictly validated, an attacker can redirect the authorization code to their own server.

python - secure redirect validation
from urllib.parse import urlparse

ALLOWED_REDIRECTS = [
    "https://app.example.com/callback",
    "https://mobile.example.com/callback",
]

def validate_redirect_uri(uri):
    parsed = urlparse(uri)
    base = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
    if base not in ALLOWED_REDIRECTS:
        raise ValueError("Invalid redirect URI")
    return True

OAuth 2.0 Best Practices

  • Always use the Authorization Code flow with PKCE for all client types.
  • Strictly validate redirect_uri against a pre-registered allowlist.
  • Use the state parameter to prevent CSRF attacks.
  • Store client secrets securely - never in client-side code or version control.
  • Use short-lived access tokens (5-15 minutes) with refresh token rotation.
  • Implement token revocation endpoints for logout and security incidents.

API Keys

API Keys are simple tokens used to identify the calling project or application. They are not a substitute for proper authentication and should not be used alone to authorize users.

Proper Usage

  • Identification, not authentication - Use API keys to identify which application is making a request, not to verify user identity.
  • Rate limiting - Associate API keys with rate limits and quotas per application.
  • Logging and analytics - Track which applications are consuming your API.
  • Access control - Restrict which API endpoints an application can access.
Important

API keys alone should never be used for user authentication. They identify the application, not the user. An API key can be leaked in client-side code, URLs, logs, or browser history.

Key Security Practices

python - secure API key handling
import os
import hmac
import hashlib
import secrets

# Generate API key with prefix for identification
def generate_api_key(prefix="kmsec"):
    random_part = secrets.token_urlsafe(32)
    return f"{prefix}_{random_part}"

# Store in environment variables, NEVER in code
api_key = os.environ["API_KEY"]

# Validate with constant-time comparison
def validate_api_key(provided_key, stored_key):
    return hmac.compare_digest(provided_key, stored_key)

# Key rotation: generate new, deprecate old
def rotate_api_key(old_key_id):
    new_key = generate_api_key()
    db.rotate_key(old_key_id, new_key, grace_period_hours=24)
    return new_key

Rotation Strategies

  • Automatic rotation - Keys expire after a set period (e.g., 90 days) and a new key is generated automatically.
  • Grace period - When rotating, allow both old and new keys to work for a short window (e.g., 24 hours).
  • Manual rotation - Triggered by security events or compromised key detection.
  • Versioned keys - Prefix keys with a version (e.g., v1_, v2_) to support rolling updates.

BOLA / IDOR (Broken Object Level Authorization)

BOLA (Broken Object Level Authorization), formerly known as IDOR (Insecure Direct Object Reference), is the #1 API security vulnerability. It occurs when an API does not properly verify that the authenticated user is authorized to access the requested object.

Vulnerability Example

http - attack
# Legitimate request - user 123 accesses their own profile
GET /api/users/123/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
# Response: 200 OK - { "name": "Alice", "email": "alice@example.com" }

# ATTACK: User changes ID to access another user's profile
GET /api/users/456/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
# Response: 200 OK - { "name": "Bob", "email": "bob@example.com" }
# The server returned another user's data!

Prevention

python - secure with authorization check
# SECURE: Verify the authenticated user owns the resource
from flask import Flask, abort, g

@app.route("/api/users/<int:user_id>/profile")
@require_auth
def get_profile(user_id):
    # CRITICAL: Check if authenticated user matches the requested resource
    if g.current_user.id != user_id and not g.current_user.is_admin:
        abort(403, description="Not authorized to access this resource")

    user = db.get_user(user_id)
    if not user:
        abort(404)

    return user.to_dict()
Prevention Tips

Always derive the resource from the authenticated session, never from user-supplied input. Use UUIDs instead of sequential IDs to make enumeration harder, but always enforce server-side authorization checks regardless.

Mass Assignment

Mass Assignment (also called auto-binding) occurs when a framework automatically maps user input to internal objects without filtering which fields should be modifiable. An attacker can inject unintended fields to escalate privileges or modify protected data.

Vulnerability Example

http - attack
# Registration request with injected "admin" field
POST /api/register
Content-Type: application/json

{
    "username": "attacker",
    "email": "attacker@evil.com",
    "password": "P@ssw0rd123",
    "admin": true,          <-- Injected field!
    "verified": true,       <-- Injected field!
    "balance": 999999      <-- Injected field!
}

Prevention

python - secure with allowlist
# SECURE: Whitelist allowed fields explicitly
from flask import request

ALLOWED_FIELDS = {"username", "email", "password", "display_name"}

@app.route("/api/register", methods=["POST"])
def register():
    data = request.get_json()

    # Filter to only allowed fields
    safe_data = {k: v for k, v in data.items() if k in ALLOWED_FIELDS}

    user = User(**safe_data)  # Only whitelisted fields are set
    db.session.add(user)
    db.session.commit()
    return {"id": user.id}, 201
Framework-Specific Protection

In Ruby on Rails, use Strong Parameters. In Django, use ModelForm or serializers. In Express.js, explicitly pick fields from the request body. Never pass raw request data directly to ORM models or database queries.

Rate Limiting

Without rate limiting, API endpoints are vulnerable to brute-force attacks, denial-of-service (DoS), credential stuffing, and resource exhaustion. Rate limiting restricts the number of requests a client can make within a given time window.

Implementation Strategies

python - Flask rate limiting
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
    get_remote_address,
    app=app,
    storage_uri="redis://localhost:6379",
    default_limits=["200 per day", "50 per hour"]
)

# Stricter limits for auth endpoints
@app.route("/api/login", methods=["POST"])
@limiter.limit("5 per minute")  # 5 attempts per minute
def login():
    ...

# Generous limits for read endpoints
@app.route("/api/products")
@limiter.limit("100 per minute")
def list_products():
    ...
nginx - rate limiting config
# Nginx rate limiting configuration
http {
    # Define rate limit zones
    limit_req_zone $binary_remote_addr zone=api_general:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=api_auth:10m rate=3r/m;

    server {
        # General API endpoints
        location /api/ {
            limit_req zone=api_general burst=20 nodelay;
            limit_req_status 429;
        }

        # Auth endpoints - stricter
        location /api/auth/ {
            limit_req zone=api_auth burst=3 nodelay;
        }
    }
}

Rate Limit Response Headers

Always include informative headers so clients know their limits:

http - response headers
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1699876543
Retry-After: 60
Tip

Use different rate limits per endpoint sensitivity: strict for authentication (3-5/minute), moderate for writes (30-60/minute), and generous for reads (100-500/minute). Always rate limit on authenticated user ID when possible, not just IP address.

Injection in APIs

Injection attacks occur when untrusted user input is included in commands, queries, or expressions without proper sanitization. APIs are particularly vulnerable because they accept structured data that is processed by backend systems.

SQL Injection via API Parameters

python - vulnerable vs secure
# VULNERABLE: String concatenation in query
def get_users(role):
    query = f"SELECT * FROM users WHERE role = '{role}'"
    db.execute(query)
    # Attack: role = "admin' OR '1'='1" → returns ALL users

# SECURE: Parameterized query
def get_users(role):
    query = "SELECT * FROM users WHERE role = %s"
    db.execute(query, (role,))  # Parameterized - safe

NoSQL Injection

javascript - MongoDB injection
// VULNERABLE: Direct use of user input in MongoDB query
const user = await db.collection('users').findOne({
    username: req.body.username,
    password: req.body.password
});
// Attack: { "username": "admin", "password": {"$gt": ""} } → matches any password

// SECURE: Validate and sanitize input types
if (typeof req.body.username !== 'string' ||
    typeof req.body.password !== 'string') {
    return res.status(400).json({ error: 'Invalid input' });
}

const user = await db.collection('users').findOne({
    username: req.body.username,
    password: hashPassword(req.body.password)
});

Command Injection

python - command injection
# VULNERABLE: User input in shell command
import os

def ping_host(hostname):
    os.system(f"ping -c 1 {hostname}")
    # Attack: hostname = "8.8.8.8; cat /etc/passwd"

# SECURE: Use subprocess with argument list (no shell)
import subprocess

def ping_host(hostname):
    # Validate input first
    if not re.match(r"^[a-zA-Z0-9.\-]+$", hostname):
        raise ValueError("Invalid hostname")

    result = subprocess.run(
        ["ping", "-c", "1", hostname],
        capture_output=True,
        timeout=10
    )
    return result.stdout.decode()

OWASP API Security Top 10 (2023)

The OWASP API Security Top 10 (2023) is the industry standard for understanding and mitigating the most critical API security risks. Unlike the general OWASP Top 10, this list specifically targets API vulnerabilities.

API1

API1:2023 - Broken Object Level Authorization

APIs tend to expose object identifiers, creating a wide attack surface. BOLA allows attackers to access unauthorized resources by manipulating object IDs.

API2

API2:2023 - Broken Authentication

Authentication mechanisms are often implemented incorrectly, allowing attackers to compromise tokens, brute-force endpoints, or exploit other flaws to impersonate users.

API3

API3:2023 - Broken Object Property Level Authorization

This category encompasses excessive data exposure and mass assignment vulnerabilities, where APIs expose more object properties than intended or allow modification of unauthorized properties.

API4

API4:2023 - Unrestricted Resource Consumption

Lack of proper rate limiting, pagination limits, or body size restrictions allows attackers to cause denial-of-service through resource exhaustion or excessive costs.

API5

API5:2023 - Broken Function Level Authorization

Complex access control policies are often not properly enforced, allowing regular users to access administrative functions or perform operations beyond their authorization level.

API6

API6:2023 - Unrestricted Access to Sensitive Business Flows

APIs that automate business processes lack protections against automated abuse, allowing attackers to exploit sensitive flows like bulk purchasing, scraping, or account creation at scale.

API7

API7:2023 - Server Side Request Forgery (SSRF)

SSRF flaws occur when APIs fetch remote resources without validating the user-supplied URL, allowing attackers to coerce the server to make requests to internal services, cloud metadata, or other unintended destinations.

API8

API8:2023 - Security Misconfiguration

Insecure default configurations, missing security headers, unnecessary features enabled, verbose error messages, and missing HTTPS enforcement all contribute to API security misconfigurations.

API9

API9:2023 - Improper Inventory Management

Organizations often lack visibility into all their APIs, including older versions, shadow APIs, and undocumented endpoints. These unmonitored APIs expand the attack surface significantly.

API10

API10:2023 - Unsafe Consumption of APIs

Many applications consume third-party APIs without proper security validation, trusting their responses blindly. If a consumed API is compromised, the attacker can inject malicious data into the consuming application.

Best Practices

A comprehensive API security strategy requires defense in depth. Follow these best practices to build resilient, secure APIs.

1. Use HTTPS Everywhere

All API communication must be encrypted in transit. Enforce TLS 1.2+ and use HSTS headers to prevent downgrade attacks.

nginx - TLS configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;

# HSTS header
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

2. Implement Proper Authentication and Authorization

  • Use industry-standard protocols: OAuth 2.0, OpenID Connect, or JWT.
  • Enforce authorization checks on every endpoint - never rely on client-side controls.
  • Implement role-based access control (RBAC) or attribute-based access control (ABAC).
  • Use short-lived tokens with refresh token rotation.

3. Validate and Sanitize All Inputs

python - input validation with Pydantic
from pydantic import BaseModel, EmailStr, Field
from typing import Optional

class CreateUserRequest(BaseModel):
    username: str = Field(..., min_length=3, max_length=50, regex=r"^[a-zA-Z0-9_]+$")
    email: EmailStr
    password: str = Field(..., min_length=12)
    display_name: Optional[str] = Field(None, max_length=100)

    # Fields NOT defined here cannot be passed in
    # This prevents mass assignment automatically

4. Implement Rate Limiting and Throttling

  • Apply per-user and per-IP rate limits.
  • Use different limits for different endpoint sensitivities.
  • Return proper 429 Too Many Requests responses with Retry-After headers.
  • Monitor for abnormal usage patterns that may indicate attacks.

5. Use an API Gateway

An API gateway centralizes security concerns like authentication, rate limiting, request validation, and logging. Options include Kong, AWS API Gateway, Azure API Management, and Traefik.

6. Implement Proper Error Handling

Never expose internal implementation details in error responses:

python - error handling
# VULNERABLE: Exposes internal details
@app.errorhandler(500)
def handle_error(e):
    return {"error": str(e), "traceback": traceback.format_exc()}, 500

# SECURE: Generic error with correlation ID
@app.errorhandler(500)
def handle_error(e):
    correlation_id = str(uuid.uuid4())
    logger.error(f"Internal error: {e}", extra={"correlation_id": correlation_id})
    return {
        "error": "Internal server error",
        "correlation_id": correlation_id
    }, 500

7. API Versioning

Always version your APIs to maintain backward compatibility and allow clients to migrate gradually. Deprecate old versions with clear timelines and communicate changes proactively.

8. Logging and Monitoring

  • Log all authentication events, access control failures, and input validation errors.
  • Use structured logging for machine-parseable audit trails.
  • Set up real-time alerting for anomalous patterns (spike in 401s, unusual data access).
  • Implement correlation IDs across distributed services for request tracing.

9. Use OpenAPI/Swagger Specification

Define your API using OpenAPI specification. Use schema validation middleware to automatically reject requests that don't conform to the spec, and generate accurate client SDKs.

Summary Checklist

[ ] HTTPS enforced   [ ] Authentication on all endpoints   [ ] Authorization checks per resource   [ ] Input validation with allowlists   [ ] Rate limiting implemented   [ ] API gateway in place   [ ] Error handling sanitized   [ ] Versioning strategy   [ ] Logging and monitoring   [ ] OpenAPI spec defined

Resources

Continue your API security learning with these trusted resources and references.

Official References

Tools for API Security Testing

Learning

Standards and Guidelines