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.
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.
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:
// 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.
# 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")
# 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.
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.
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.
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:
# 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.
# 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.
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_uriagainst a pre-registered allowlist. - Use the
stateparameter 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.
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
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
# 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
# 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()
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
# 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
# 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
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
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 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/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1699876543
Retry-After: 60
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
# 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
// 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
# 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: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: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: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: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: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: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: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: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: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: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.
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
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 Requestsresponses withRetry-Afterheaders. - 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:
# 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.
[ ] 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
- OWASP API Security Project - The definitive resource for API security risks and mitigations.
- OWASP API Security Top 10 (2023) - The latest edition of the API Security Top 10.
- OpenAPI Specification - Standard for defining REST APIs.
- JWT.io - JWT debugger and documentation.
- RFC 6749 - OAuth 2.0 Framework - The official OAuth 2.0 specification.
Tools for API Security Testing
- Burp Suite - Industry-standard web and API security testing proxy.
- Postman - API development and testing platform.
- OWASP Web Security Testing Guide - Comprehensive testing methodology.
Learning
Standards and Guidelines
- NIST SP 800-204 - Security Strategies for Microservices.
- OWASP Cheat Sheet Series - Quick-reference security guides.