1. Introduction
APIs are the connective tissue of modern software — mobile apps, SPAs, microservices, IoT devices and third-party integrations all depend on them. A single vulnerability in an API can expose millions of records, disrupt entire supply chains, or allow lateral movement through your infrastructure.
This guide presents practical, language-agnostic patterns you can apply across REST, GraphQL, gRPC and event-driven architectures. Every section includes code examples, real-world context and a clear "why" so you can prioritize improvements based on your risk profile.
2. Why API Security Matters
APIs are the #1 attack vector for web applications. Consider these facts:
- 91 % of web applications have at least one API vulnerability (Salt Security, 2024).
- The average cost of an API-related data breach exceeds $6.1 million (IBM Cost of a Data Breach Report).
- APIs bypass traditional WAFs and perimeter defenses because they are designed to be accessible.
- Attack surface grows linearly with every new endpoint, version and partner integration.
Secure API design is not a checkbox — it is a continuous discipline that spans architecture, development, deployment and operations.
3. OWASP API Security Top 10
The OWASP API Security Top 10 (2023 edition) identifies the most critical API risks. Every section in this guide maps to one or more of these:
- API1 — Broken Object-Level Authorization (BOLA) — Accessing resources belonging to other users by manipulating IDs.
- API2 — Broken Authentication — Weak or missing authentication mechanisms.
- API3 — Broken Object Property-Level Authorization — Exposing or allowing modification of sensitive object properties (mass assignment).
- API4 — Unrestricted Resource Consumption — Missing rate limits or resource quotas.
- API5 — Broken Function-Level Authorization — Accessing admin functions without proper role checks.
- API6 — Unrestricted Access to Sensitive Business Flows — Automating abuse of business logic (e.g. bulk ticket purchasing).
- API7 — Server-Side Request Forgery (SSRF) — Server fetches attacker-controlled URLs.
- API8 — Security Misconfiguration — Missing security headers, verbose errors, default credentials.
- API9 — Improper Inventory Management — Stale or undocumented endpoints left exposed.
- API10 — Unsafe Consumption of APIs — Blindly trusting responses from third-party APIs.
We will cover defenses for every one of these throughout the guide.
4. Core Principles
4.1 Least Privilege
Grant the minimum access necessary for each component, user, or service account. A database connection used by the "orders" service should only have SELECT/INSERT on the orders table — never DROP or access to users.
4.2 Defense in Depth
Layer multiple independent controls: authentication → authorization → input validation → rate limiting → logging → network segmentation. If one layer fails, the others still contain the blast radius.
4.3 Fail Securely
When something goes wrong, deny by default. Return generic error messages to callers while recording detailed diagnostics server-side. Never expose stack traces, database errors, or internal paths.
4.4 Zero Trust
Treat every network segment — including internal — as hostile. Authenticate and authorize every request, even between internal services. Never rely on "it's behind the firewall" as a security control.
4.5 Secure by Default
Choose defaults that maximize security: TLS enabled, authentication required, rate limits active, verbose errors off, CORS restricted to known origins. Make insecure configurations require explicit opt-in.
5. Authentication Mechanisms
5.1 API Keys
API keys are simple but limited — they identify the application, not the user. Use them for server-to-server calls where user context is not needed, and always send them in headers (never in URLs, which get logged).
# ✅ Good: API key in header
GET /api/v1/data HTTP/1.1
Host: api.example.com
X-API-Key: sk_live_abc123...
# ❌ Bad: API key in URL (gets logged, cached, shared in Referer headers)
GET /api/v1/data?api_key=sk_live_abc123...
5.2 OAuth 2.0 & OpenID Connect
OAuth 2.0 is the industry standard for delegated authorization. Use it when a user grants your app access to their resources on another service. OpenID Connect (OIDC) adds an identity layer on top for authentication.
- Authorization Code + PKCE — For SPAs and mobile apps. Always use PKCE to prevent code interception.
- Client Credentials — For server-to-server (machine-to-machine) flows with no user context.
- Device Authorization — For IoT devices with limited input capabilities.
# Authorization Code + PKCE flow (simplified)
# 1. Generate code_verifier (random 43-128 chars) and code_challenge = BASE64URL(SHA256(code_verifier))
# 2. Redirect user to authorization endpoint:
GET /authorize?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://app.example.com/callback
&scope=openid profile orders:read
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
# 3. Exchange code for tokens:
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTH_CODE_HERE
&redirect_uri=https://app.example.com/callback
&client_id=YOUR_CLIENT_ID
&code_verifier=YOUR_CODE_VERIFIER
5.3 JSON Web Tokens (JWT)
JWTs are compact, self-contained tokens commonly used as OAuth 2.0 access tokens. Key rules:
- Always validate
iss(issuer),aud(audience),exp(expiration) andnbf(not before). - Use asymmetric algorithms (
RS256,ES256) — nevernoneorHS256with a shared secret in public clients. - Keep payloads small — JWTs travel on every request.
- Do not store sensitive data in the JWT body (it is Base64-encoded, not encrypted).
// JWT validation (Node.js pseudocode)
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const client = jwksClient({ jwksUri: 'https://auth.example.com/.well-known/jwks.json' });
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
callback(null, key.getPublicKey());
});
}
function verifyToken(token) {
return new Promise((resolve, reject) => {
jwt.verify(token, getKey, {
algorithms: ['RS256'],
issuer: 'https://auth.example.com/',
audience: 'https://api.example.com'
}, (err, decoded) => {
if (err) return reject(err);
resolve(decoded);
});
});
}
5.4 Mutual TLS (mTLS)
Both client and server present certificates. mTLS provides strong machine identity for service-to-service communication and is the foundation of service meshes (Istio, Linkerd). Use it when API keys or JWTs are insufficient for your threat model.
6. Token Management
6.1 Short-Lived Access Tokens
Access tokens should expire in 5–15 minutes. Short lifetimes limit the window of exploitation if a token is leaked. Use refresh tokens (stored securely) for session continuity.
6.2 Refresh Token Rotation
Issue a new refresh token on every use and invalidate the old one (one-time use). If a stolen refresh token is replayed, the rotation detects it and revokes the entire session.
// Refresh token rotation flow
POST /token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=OLD_REFRESH_TOKEN
&client_id=YOUR_CLIENT_ID
// Response:
{
"access_token": "eyJ...",
"refresh_token": "NEW_REFRESH_TOKEN", // old one is now invalid
"expires_in": 900
}
6.3 Token Revocation
Implement a token revocation endpoint (RFC 7009) and maintain a revocation list or use token introspection for server-side validation. This is critical for logout, password changes and account compromise scenarios.
6.4 Token Storage
- Browser SPAs — Store tokens in memory (not
localStorage). UsehttpOnly/secure/SameSite=Strictcookies as a transport mechanism when possible. - Mobile apps — Use platform secure storage (iOS Keychain, Android Keystore).
- Server-side — Keep tokens in encrypted session stores or secure vaults.
7. Authorization Patterns
7.1 Role-Based Access Control (RBAC)
Assign users to roles (admin, editor, viewer) and check roles on each request. Simple and effective for most applications.
// RBAC middleware (Express-like)
function requireRole(...roles) {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
app.delete('/api/users/:id', requireRole('admin'), deleteUser);
7.2 Attribute-Based Access Control (ABAC)
Evaluate policies based on attributes of the user, resource, action and environment. More flexible than RBAC for complex scenarios (e.g. "editors can only modify articles in their department during business hours").
// ABAC policy evaluation (pseudocode)
function evaluatePolicy(user, resource, action, environment) {
const policies = getPolicies(resource.type, action);
return policies.every(policy => {
if (policy.userDept && policy.userDept !== user.department) return false;
if (policy.timeWindow && !isWithin(environment.time, policy.timeWindow)) return false;
if (policy.ownership && resource.ownerId !== user.id) return false;
return true;
});
}
7.3 Preventing BOLA (Broken Object-Level Authorization)
BOLA is the #1 API vulnerability. It occurs when an API returns or modifies a resource without verifying that the authenticated user owns or has access to it.
// ❌ Vulnerable: returns any order by ID
app.get('/api/orders/:id', async (req, res) => {
const order = await db.orders.findById(req.params.id);
res.json(order);
});
// ✅ Secure: verifies ownership
app.get('/api/orders/:id', async (req, res) => {
const order = await db.orders.findById(req.params.id);
if (!order || order.userId !== req.user.id) {
return res.status(404).json({ error: 'Not found' }); // 404, not 403
}
res.json(order);
});
Tip: Return 404 instead of 403 for unauthorized resources to prevent ID enumeration.
8. Input Validation
8.1 Schema-Driven Validation
Validate every input — path parameters, query strings, headers and request bodies — against strict schemas. Use JSON Schema, OpenAPI, Zod, Joi or your framework's built-in validation. Reject unknown fields (additionalProperties: false).
// JSON Schema for creating an order
{
"type": "object",
"required": ["productId", "quantity"],
"additionalProperties": false,
"properties": {
"productId": {
"type": "string",
"pattern": "^[a-zA-Z0-9-]{1,36}$"
},
"quantity": {
"type": "integer",
"minimum": 1,
"maximum": 100
},
"notes": {
"type": "string",
"maxLength": 500
}
}
}
8.2 Allow-Lists Over Deny-Lists
Define what is allowed rather than what is blocked. Deny-lists are always incomplete — attackers find novel bypass patterns. Validate data types, lengths, ranges and character sets explicitly.
8.3 Content-Type Enforcement
Always validate the Content-Type header matches what the endpoint expects. Reject requests with unexpected content types to prevent type-confusion attacks.
// Content-Type enforcement middleware
function requireJson(req, res, next) {
const ct = req.headers['content-type'] || '';
if (!ct.includes('application/json')) {
return res.status(415).json({ error: 'Unsupported Media Type. Expected application/json' });
}
next();
}
app.post('/api/orders', requireJson, validateSchema(orderSchema), createOrder);
8.4 Path & Query Parameter Validation
Most validation libraries focus on bodies but forget path and query params. Validate :id parameters as UUIDs or integers, enforce limit/offset ranges, and reject unexpected query parameters.
// Path parameter validation
app.get('/api/orders/:id', (req, res, next) => {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(req.params.id)) {
return res.status(400).json({ error: 'Invalid order ID format' });
}
next();
});
9. Output Encoding & Content-Type
Secure output is as important as secure input:
- Set explicit Content-Type — Always return
Content-Type: application/json; charset=utf-8(nevertext/htmlfor API responses) to prevent browser interpretation. - Encode data in context — If API responses are rendered in HTML (e.g. admin dashboards), HTML-encode user-supplied fields to prevent stored XSS.
- Strip sensitive fields — Never include passwords, internal IDs, database keys or tokens in responses. Use DTOs/serializers to control exactly which fields are returned.
- Avoid reflection — Do not echo back raw user input in error messages (prevents reflected XSS/injection).
// DTO pattern: control what's returned
function toOrderResponse(order) {
return {
id: order.publicId, // public UUID, not internal DB id
product: order.productName,
quantity: order.quantity,
status: order.status,
createdAt: order.createdAt
// ❌ Never include: order.internalId, order.userId, order.paymentToken
};
}
app.get('/api/orders/:id', async (req, res) => {
const order = await findOrder(req.params.id, req.user.id);
res.json(toOrderResponse(order));
});
10. Rate Limiting & Throttling
10.1 Why Rate Limit?
Rate limiting protects against brute-force attacks, credential stuffing, denial-of-service, resource exhaustion and abuse of business logic (e.g. scraping prices, bulk account creation).
10.2 Strategies
- Token bucket — Smooth, allows short bursts. Ideal for general API traffic.
- Sliding window — More precise than fixed windows, avoids boundary-spike issues.
- Leaky bucket — Enforces a strict, constant rate. Good for webhooks and queues.
10.3 Granularity
Apply rate limits at multiple levels:
- Per-user — Based on authenticated identity (token claims).
- Per-IP — Fallback for unauthenticated endpoints.
- Per-API-key — For third-party consumers.
- Per-endpoint — Sensitive endpoints (login, password reset) get stricter limits.
10.4 Standard Headers
Communicate quota information to clients using standard headers:
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1709312400
Retry-After: 30 # seconds (only on 429 responses)
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json
{ "error": "Rate limit exceeded. Retry after 30 seconds." }
10.5 Token Bucket Implementation
// Token bucket (Node.js pseudocode)
const buckets = new Map();
function rateLimiter(maxTokens, refillRate) {
return (req, res, next) => {
const key = req.user?.id || req.ip;
let bucket = buckets.get(key) || { tokens: maxTokens, lastRefill: Date.now() };
// Refill tokens based on elapsed time
const now = Date.now();
const elapsed = (now - bucket.lastRefill) / 1000;
bucket.tokens = Math.min(maxTokens, bucket.tokens + elapsed * refillRate);
bucket.lastRefill = now;
if (bucket.tokens < 1) {
res.set('Retry-After', Math.ceil((1 - bucket.tokens) / refillRate));
return res.status(429).json({ error: 'Rate limit exceeded' });
}
bucket.tokens--;
buckets.set(key, bucket);
res.set('X-RateLimit-Remaining', Math.floor(bucket.tokens));
next();
};
}
// 100 requests per minute per user
app.use('/api/', rateLimiter(100, 100 / 60));
11. CORS Configuration
11.1 What Is CORS?
Cross-Origin Resource Sharing is a browser security mechanism that controls which origins can make requests to your API. Misconfigured CORS is a top source of API vulnerabilities.
11.2 Secure Configuration Rules
- Never use
Access-Control-Allow-Origin: *with credentials — it is forbidden by the spec and browsers will block it, but some developers disable CORS entirely as a "fix". - Maintain an allow-list of trusted origins. Validate the
Originheader against the list — never reflect it back blindly. - Restrict methods and headers — Only expose the HTTP methods and headers your API actually uses.
- Limit
Access-Control-Max-Age— Cache preflight responses, but keep the duration reasonable (e.g. 600–3600 seconds).
// Secure CORS configuration (Express)
const allowedOrigins = new Set([
'https://app.example.com',
'https://admin.example.com'
]);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.has(origin)) {
res.set('Access-Control-Allow-Origin', origin);
res.set('Access-Control-Allow-Credentials', 'true');
res.set('Vary', 'Origin');
}
res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.set('Access-Control-Allow-Headers', 'Authorization, Content-Type');
res.set('Access-Control-Max-Age', '600');
if (req.method === 'OPTIONS') return res.status(204).end();
next();
});
11.3 Common CORS Mistakes
- Reflecting the
Originheader without validation (bypasses CORS entirely). - Using regex that matches substrings (e.g.
example.comalso matchesevil-example.com). - Forgetting the
Vary: Originheader, causing CDN cache poisoning. - Disabling CORS in development and forgetting to re-enable it in production.
12. HTTPS & TLS
12.1 Enforce TLS Everywhere
All API traffic — external and internal — must use TLS 1.2 or higher. TLS 1.0 and 1.1 are deprecated. Redirect HTTP to HTTPS at the load balancer or gateway level.
12.2 HSTS (HTTP Strict Transport Security)
# Enable HSTS — tells browsers to ONLY use HTTPS for 1 year
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Add your domain to the HSTS preload list for maximum protection.
12.3 TLS Configuration Best Practices
- Use strong cipher suites (AES-256-GCM, ChaCha20-Poly1305).
- Disable weak ciphers (RC4, DES, 3DES, CBC-mode without AEAD).
- Enable OCSP stapling for faster certificate validation.
- Automate certificate renewal (Let's Encrypt, cert-manager).
- Pin certificates or public keys for mobile apps communicating with your own APIs.
13. Security Headers
Every API response should include these headers to reduce attack surface:
# Recommended security headers for API responses
Content-Type: application/json; charset=utf-8
X-Content-Type-Options: nosniff # prevent MIME sniffing
X-Frame-Options: DENY # prevent framing (clickjacking)
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'
Cache-Control: no-store # don't cache sensitive responses
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Request-Id: 550e8400-e29b-41d4-a716 # correlation ID for tracing
Remove revealing headers: Strip Server, X-Powered-By, X-AspNet-Version and similar headers that disclose technology stack information.
14. Error Handling
14.1 Generic Client-Facing Errors
Never expose internal details. Return consistent, machine-readable error responses:
// ✅ Good: generic, structured error
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [
{ "field": "email", "issue": "Must be a valid email address" }
],
"requestId": "550e8400-e29b-41d4-a716"
}
}
// ❌ Bad: leaks internals
{
"error": "SequelizeDatabaseError: relation \"users\" does not exist",
"stack": "at Query.run (/app/node_modules/sequelize/...)"
}
14.2 Correct HTTP Status Codes
400— Bad Request (validation errors).401— Unauthorized (missing or invalid authentication).403— Forbidden (authenticated but insufficient permissions).404— Not Found (also use for unauthorized resource access to prevent enumeration).415— Unsupported Media Type (wrong Content-Type).422— Unprocessable Entity (valid syntax but semantic errors).429— Too Many Requests (rate limit exceeded).500— Internal Server Error (catch-all, log details server-side).
14.3 Error Logging vs. Error Response
Log the full error (stack trace, request details, user context) server-side for debugging. Send only a sanitized summary and requestId to the client so support teams can correlate reports with logs.
15. Injection Prevention
15.1 SQL Injection
The most well-known injection attack. Always use parameterized queries or ORM methods — never concatenate user input into SQL strings.
// ❌ Vulnerable to SQL injection
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`;
db.query(query);
// ✅ Parameterized query (Node.js + pg)
const result = await db.query(
'SELECT * FROM users WHERE email = $1',
[req.body.email]
);
// ✅ ORM approach (Sequelize)
const user = await User.findOne({ where: { email: req.body.email } });
15.2 NoSQL Injection
NoSQL databases (MongoDB, etc.) are also vulnerable. Attackers inject operators like $gt, $ne or $regex through JSON payloads.
// ❌ Vulnerable: attacker sends { "email": { "$ne": "" } } to match all users
const user = await db.collection('users').findOne({ email: req.body.email });
// ✅ Secure: validate type before query
if (typeof req.body.email !== 'string') {
return res.status(400).json({ error: 'Email must be a string' });
}
const user = await db.collection('users').findOne({ email: req.body.email });
15.3 Command Injection
Never pass user input to shell commands. If you must execute system commands, use safe APIs that accept argument arrays (no shell interpolation).
// ❌ Vulnerable: shell injection via filename
const { exec } = require('child_process');
exec(`convert ${req.body.filename} output.png`); // filename: "image.png; rm -rf /"
// ✅ Secure: use execFile with argument array (no shell)
const { execFile } = require('child_process');
execFile('convert', [req.body.filename, 'output.png']);
15.4 Log Injection
Attackers inject newlines or ANSI escape codes into log entries to forge log entries or cause confusion. Sanitize log inputs by replacing control characters.
// Sanitize user input before logging
function sanitizeForLog(input) {
return String(input).replace(/[\n\r\t\x00-\x1f]/g, '_');
}
logger.info(`Login attempt for user: ${sanitizeForLog(req.body.username)}`);
16. SSRF Prevention
Server-Side Request Forgery (SSRF) occurs when an attacker tricks your server into making requests to internal resources or arbitrary external URLs.
16.1 Common SSRF Vectors
- Webhook URLs configured by users.
- Image/file URL imports (e.g. "import avatar from URL").
- PDF generation from user-supplied URLs.
- API integrations that fetch remote resources.
16.2 Defenses
// SSRF prevention: validate and restrict URLs
const { URL } = require('url');
const dns = require('dns').promises;
const ipaddr = require('ipaddr.js');
async function validateUrl(userUrl) {
const parsed = new URL(userUrl);
// 1. Only allow HTTPS
if (parsed.protocol !== 'https:') throw new Error('Only HTTPS allowed');
// 2. Block private/reserved IPs
const addresses = await dns.resolve4(parsed.hostname);
for (const addr of addresses) {
const ip = ipaddr.parse(addr);
const range = ip.range();
if (['private', 'loopback', 'linkLocal', 'uniqueLocal'].includes(range)) {
throw new Error('Internal addresses are not allowed');
}
}
// 3. Optional: allow-list of domains
const allowedDomains = ['api.partner.com', 'cdn.example.com'];
if (!allowedDomains.includes(parsed.hostname)) {
throw new Error('Domain not in allow-list');
}
return parsed.href;
}
17. Mass Assignment Protection
Mass assignment occurs when an API blindly maps client-sent fields to database models. An attacker adds fields like role: "admin" or price: 0 to the request body.
17.1 Defenses
// ❌ Vulnerable: spread entire body into database
app.put('/api/users/:id', async (req, res) => {
await db.users.update(req.params.id, req.body);
// Attacker sends: { "name": "Hacker", "role": "admin", "verified": true }
});
// ✅ Secure: explicit allow-list (DTO pattern)
app.put('/api/users/:id', async (req, res) => {
const allowed = {
name: req.body.name,
email: req.body.email,
avatar: req.body.avatar
// role, verified, etc. are NEVER accepted from the client
};
await db.users.update(req.params.id, allowed);
});
Combine with additionalProperties: false in JSON Schema to reject unknown fields at the validation layer.
18. Pagination & Filtering
18.1 Prevent Resource Exhaustion
Without pagination limits, an attacker can request all records at once, causing memory exhaustion and slow queries.
// Enforce pagination limits
app.get('/api/orders', (req, res) => {
const limit = Math.min(parseInt(req.query.limit) || 20, 100); // max 100
const offset = Math.max(parseInt(req.query.offset) || 0, 0);
// Use limit + offset in the database query
});
// Even better: cursor-based pagination (avoids offset performance issues)
app.get('/api/orders', (req, res) => {
const limit = Math.min(parseInt(req.query.limit) || 20, 100);
const cursor = req.query.cursor; // encoded last-seen ID
// SELECT * FROM orders WHERE id > cursor ORDER BY id LIMIT limit + 1
});
18.2 Filter Injection
If your API accepts filter expressions (e.g. ?filter=price gt 100), validate and restrict allowed fields and operators. Never pass raw filter strings to the database.
19. File Upload Security
- Validate MIME type server-side by reading magic bytes — never trust the
Content-Typeheader alone. - Limit file size at the reverse proxy and application layers.
- Rename files — use UUIDs, never the original filename (prevents path traversal).
- Store outside the web root — serve through a separate CDN or signed URL.
- Scan for malware before processing or storing.
- Set
Content-Disposition: attachmentwhen serving downloads to prevent browser execution.
// File upload handler with validation
const multer = require('multer');
const { fileTypeFromBuffer } = require('file-type');
const upload = multer({
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB max
storage: multer.memoryStorage()
});
app.post('/api/upload', upload.single('file'), async (req, res) => {
// Validate actual file type (magic bytes)
const type = await fileTypeFromBuffer(req.file.buffer);
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (!type || !allowedTypes.includes(type.mime)) {
return res.status(415).json({ error: 'File type not allowed' });
}
// Generate safe filename
const filename = `${crypto.randomUUID()}.${type.ext}`;
await storage.upload(filename, req.file.buffer);
res.status(201).json({ url: `/cdn/${filename}` });
});
20. API Versioning
20.1 Strategies
- URI path —
/api/v1/orders— most common, easy to route. - Header —
Accept: application/vnd.example.v2+json— cleaner URLs but harder to test with browsers. - Query parameter —
/api/orders?version=2— easy but pollutes caching.
20.2 Security Implications
Old API versions are a major security risk — they may lack fixes applied to newer versions. Maintain a deprecation policy:
- Document version sunset dates in advance.
- Return
SunsetandDeprecationheaders on deprecated versions. - Apply security patches to all still-active versions.
- Monitor traffic to deprecated versions and alert before shutdown.
- Keep an inventory of all API versions, endpoints and consumers (addresses OWASP API9).
21. API Gateway Patterns
21.1 Centralized Enforcement
An API gateway sits in front of your services and enforces security policies uniformly:
- TLS termination and certificate management.
- Authentication (token validation, API key verification).
- Rate limiting and throttling.
- Request/response validation (schema validation, size limits).
- IP allow/deny lists and geo-blocking.
- Request transformation and header injection.
21.2 Web Application Firewall (WAF)
Deploy a WAF in front of or alongside the gateway to detect and block common attack patterns (SQL injection, XSS, path traversal) before they reach your application code.
21.3 Microgateway Pattern
For microservice architectures, deploy lightweight gateways (sidecars) alongside each service. The sidecar handles mTLS, auth, and rate limiting locally — reducing latency and single-point-of-failure risks.
22. Service-to-Service Security
22.1 Mutual TLS (mTLS)
Both client and server present X.509 certificates. Service meshes (Istio, Linkerd, Consul Connect) automate certificate issuance, rotation and mTLS enforcement transparently.
22.2 Workload Identity
In cloud environments, use platform-native identity (GCP Workload Identity, AWS IAM Roles for Service Accounts, Azure Managed Identity) instead of static credentials. The platform rotates credentials automatically.
22.3 Service-to-Service JWT
For environments without mTLS, services can exchange short-lived JWTs signed with asymmetric keys. Each service validates the issuer and audience before processing.
// Service A calls Service B with a service JWT
const serviceToken = signJwt({
iss: 'service-a',
aud: 'service-b',
sub: 'service-a',
exp: Math.floor(Date.now() / 1000) + 300 // 5 min
}, SERVICE_A_PRIVATE_KEY, { algorithm: 'ES256' });
const response = await fetch('https://service-b.internal/api/data', {
headers: { 'Authorization': `Bearer ${serviceToken}` }
});
23. Secrets Management
23.1 Never Hardcode Secrets
API keys, database passwords, encryption keys and signing keys must never appear in source code, configuration files committed to version control, or container images.
23.2 Use a Secrets Vault
Use dedicated tools: HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager or Doppler. These provide:
- Encryption at rest and in transit.
- Access control and audit logging.
- Automatic rotation and versioning.
- Dynamic secrets (generate short-lived DB credentials on demand).
23.3 Environment Variables — With Caution
Environment variables are better than hardcoding but still risky — they can leak via /proc, error pages, debug endpoints or crash dumps. Prefer vault injection at runtime.
23.4 Secret Scanning
Enable automated secret scanning in your CI pipeline to detect accidentally committed secrets:
# Example: run Gitleaks in CI
gitleaks detect --source . --verbose
# Example: GitHub secret scanning
# Automatically enabled for public repos; enable push protection for private repos
24. Webhook Security
24.1 HMAC Signature Verification
Webhook providers (Stripe, GitHub, Slack) sign payloads with an HMAC secret. Always verify the signature before processing the webhook.
// Webhook HMAC verification (Node.js)
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
const sig = Buffer.from(signature, 'hex');
const exp = Buffer.from(expected, 'hex');
if (sig.length !== exp.length) return false;
return crypto.timingSafeEqual(sig, exp);
}
app.post('/webhooks/stripe', express.raw({ type: '*/*' }), (req, res) => {
const sig = req.headers['x-stripe-signature'];
if (!verifyWebhookSignature(req.body, sig, WEBHOOK_SECRET)) {
return res.status(401).end();
}
// Process webhook...
res.status(200).end();
});
24.2 Replay Prevention
Include a timestamp in the signature payload and reject webhooks older than a threshold (e.g. 5 minutes). Store processed webhook IDs to prevent duplicate processing.
24.3 Outgoing Webhook Security
If your API sends webhooks to user-configured URLs, apply the same SSRF defenses from Section 16: validate URLs, block private IPs and use allow-lists where possible.
25. GraphQL Security
25.1 Query Depth & Complexity Limiting
GraphQL's flexibility is also its risk — a single query can request deeply nested relationships or combine expensive fields.
# ❌ Dangerous: deeply nested query (N+1 on steroids)
query {
users {
posts {
comments {
author {
posts {
comments {
author { name }
}
}
}
}
}
}
}
Defenses:
- Max depth — Reject queries exceeding a depth limit (e.g. 5 levels).
- Query cost analysis — Assign costs to fields and reject queries exceeding a budget.
- Pagination required — Require
first/lastarguments on list fields.
25.2 Disable Introspection in Production
Introspection exposes your entire schema to attackers. Disable it in production or restrict it to authenticated admin users.
// Disable introspection (Apollo Server)
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production'
});
25.3 Persisted Queries
Use automatic persisted queries (APQ) to restrict execution to pre-approved queries. This prevents arbitrary query execution and reduces payload size.
26. Logging & Monitoring
26.1 Structured Logging
Use structured JSON logs for machine parsing. Include correlation IDs, timestamps, user context and action outcomes.
// Structured log entry
{
"timestamp": "2026-03-01T14:22:05.123Z",
"level": "warn",
"event": "auth_failure",
"requestId": "550e8400-e29b-41d4-a716",
"clientIp": "203.0.113.42",
"userId": null,
"path": "/api/v1/login",
"method": "POST",
"statusCode": 401,
"reason": "invalid_credentials",
"userAgent": "Mozilla/5.0..."
}
26.2 What to Log
- Authentication successes and failures.
- Authorization denials.
- Rate limit triggers.
- Input validation failures.
- Sensitive data access (read/write on PII fields).
- Configuration changes (API key creation, permission changes).
26.3 What NOT to Log
- Passwords, tokens, API keys or session IDs.
- Full credit card numbers or SSNs.
- Request/response bodies containing PII (mask or truncate).
26.4 Alerting
Set up alerts for anomalies: sudden spikes in 401/403/429 responses, unusual request volumes from a single client, access to deprecated endpoints, or error rate exceeding thresholds.
27. Audit Trails
Audit trails are immutable records of who did what, when, and from where. They are essential for compliance (SOC 2, GDPR, HIPAA, PCI-DSS) and incident investigation.
- Record: who (user ID, service ID), what (action, resource, before/after values), when (timestamp), where (IP, device).
- Store audit logs in a separate, append-only data store with restricted access.
- Retain for the period required by your regulatory framework (often 1–7 years).
- Never allow deletion or modification of audit records from the application layer.
28. CI/CD & Supply-Chain Security
28.1 Pipeline Security
- SAST (Static Analysis) — Scan source code for vulnerabilities before merging (Semgrep, SonarQube, CodeQL).
- DAST (Dynamic Analysis) — Run automated attacks against staging environments (OWASP ZAP, Burp Suite).
- SCA (Software Composition Analysis) — Detect known vulnerabilities in dependencies (Snyk, Dependabot, Trivy).
- Container scanning — Scan Docker images for OS-level vulnerabilities before deployment.
- IaC scanning — Check Terraform, CloudFormation and Kubernetes manifests for misconfigurations (Checkov, tfsec).
28.2 Dependency Management
# Lock files ensure reproducible builds and prevent supply-chain attacks
# Node.js: package-lock.json or yarn.lock
npm ci --ignore-scripts # deterministic install, skip postinstall scripts
# Python: pin exact versions
pip install -r requirements.txt --require-hashes
# Audit dependencies regularly
npm audit
pip-audit
28.3 Signed Artifacts
Sign your container images and release artifacts (Sigstore/Cosign, GPG). Verify signatures in your deployment pipeline to ensure only trusted code is deployed.
29. OpenAPI & Contract-First Design
29.1 Design First, Code Second
Write the OpenAPI specification before writing code. This ensures security considerations (authentication schemes, input schemas, rate limits) are designed upfront rather than bolted on later.
29.2 Security Schemes in OpenAPI
# OpenAPI 3.x security scheme definitions
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
apiKey:
type: apiKey
in: header
name: X-API-Key
oauth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://auth.example.com/authorize
tokenUrl: https://auth.example.com/token
scopes:
orders:read: Read orders
orders:write: Create/update orders
# Apply globally
security:
- bearerAuth: []
# Override per-endpoint
paths:
/api/v1/orders:
get:
security:
- bearerAuth: []
- apiKey: []
parameters:
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
'200':
description: List of orders
'401':
description: Missing or invalid authentication
'429':
description: Rate limit exceeded
29.3 Runtime Validation from OpenAPI
Use middleware that auto-generates validation from your OpenAPI spec (e.g. express-openapi-validator, connexion for Python, committee for Ruby). This eliminates drift between documentation and implementation.
30. Testing API Security
30.1 Security Unit Tests
Write tests that explicitly verify security controls:
// Security test examples (Jest/Supertest)
describe('Authorization', () => {
it('returns 401 without a token', async () => {
const res = await request(app).get('/api/orders');
expect(res.status).toBe(401);
});
it('returns 403 for insufficient scope', async () => {
const token = createToken({ scope: 'profile:read' }); // missing orders:read
const res = await request(app)
.get('/api/orders')
.set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(403);
});
it('returns 404 for another user\'s order (BOLA)', async () => {
const token = createToken({ userId: 'user-1' });
const res = await request(app)
.get('/api/orders/order-owned-by-user-2')
.set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(404); // not 403 — prevents enumeration
});
it('rejects unknown fields (mass assignment)', async () => {
const token = createToken({ userId: 'user-1', scope: 'users:write' });
const res = await request(app)
.put('/api/users/user-1')
.set('Authorization', `Bearer ${token}`)
.send({ name: 'Test', role: 'admin' }); // role should be rejected
expect(res.status).toBe(400);
});
});
30.2 Fuzz Testing
Send randomly generated or malformed inputs to discover unexpected behaviors. Tools: Schemathesis (generates tests from OpenAPI specs), RESTler (stateful API fuzzer by Microsoft).
# Fuzz test from OpenAPI spec with Schemathesis
schemathesis run https://api.example.com/openapi.json \
--checks all \
--hypothesis-max-examples 1000
30.3 Penetration Testing
Regularly engage security professionals or use automated tools (OWASP ZAP, Burp Suite) for comprehensive penetration testing. Focus on:
- BOLA/IDOR across all endpoints.
- Authentication bypass (token manipulation, algorithm confusion).
- Rate limit bypass (header spoofing, distributed attacks).
- Injection (SQL, NoSQL, command, LDAP).
- SSRF via URL-accepting parameters.
- Business logic abuse (race conditions, parameter tampering).
31. Security Checklist
Use this checklist during design reviews, code reviews and audits:
- ☐ All endpoints require authentication (or are explicitly marked public).
- ☐ Tokens are short-lived (< 15 min) with refresh token rotation.
- ☐ Authorization checks verify ownership/access on every request (BOLA prevention).
- ☐ Input validation is schema-driven with
additionalProperties: false. - ☐ Content-Type is validated on all endpoints that accept bodies.
- ☐ Rate limits are applied per-user, per-IP and per-endpoint.
- ☐ CORS is restricted to explicit origin allow-list.
- ☐ TLS 1.2+ enforced; HSTS enabled.
- ☐ Security headers are set on all responses.
- ☐ Error responses are generic — no stack traces, internal paths or DB errors.
- ☐ All database queries use parameterized queries or ORM methods.
- ☐ URLs provided by users are validated (SSRF prevention).
- ☐ Request/response DTOs prevent mass assignment.
- ☐ Pagination has enforced maximum limits.
- ☐ File uploads validate magic bytes and limit size.
- ☐ Deprecated API versions have sunset dates and monitoring.
- ☐ Secrets are in a vault — never in code, config files or env vars.
- ☐ Webhooks verify HMAC signatures and timestamps.
- ☐ GraphQL has depth/cost limits and introspection disabled in production.
- ☐ Logs are structured, contain no secrets and include correlation IDs.
- ☐ Audit trails are immutable and compliant with retention policies.
- ☐ CI pipeline includes SAST, DAST, SCA and container scanning.
- ☐ Dependencies are pinned and audited regularly.
- ☐ Security tests cover auth, authz, BOLA, injection and rate limits.
32. FAQ
Should I use API keys or OAuth 2.0?
Use API keys for server-to-server calls where user context is not needed and simplicity is valued. Use OAuth 2.0 when you need delegated authorization, user identity, fine-grained scopes, or token rotation. Many APIs use both: API keys for identification + OAuth for authorization.
Is JWT authentication or authorization?
JWT is a token format, not a protocol. It can carry authentication data (identity claims from OIDC) and authorization data (scopes, roles). The distinction depends on how you use it. In OAuth 2.0 flows, the JWT access token is used for authorization; the ID token (also JWT) is used for authentication.
How do I deprecate an API version safely?
1) Announce the sunset date well in advance. 2) Add Sunset and Deprecation headers to responses. 3) Monitor traffic to identify remaining consumers. 4) Contact consumers directly. 5) Return 410 Gone after the sunset date, with a migration guide URL in the response.
Is rate limiting enough to prevent DDoS?
No. Rate limiting handles application-layer abuse, but volumetric DDoS attacks require network-layer defenses: CDN/DDoS protection (Cloudflare, AWS Shield), geo-blocking, and infrastructure scaling. Use rate limiting in addition to network-layer protection.
Should I disable CORS for internal APIs?
If your API is never called from a browser, you can omit CORS headers entirely. The browser won't send cross-origin requests if your API doesn't return Access-Control-Allow-Origin. But if any web client (even internal admin panels) calls the API, configure CORS properly.
How often should I rotate secrets?
It depends on the secret type: API keys — every 90 days minimum. Database passwords — use dynamic secrets from a vault (auto-rotated per session). Signing keys — use JWKS with key rotation (overlap old and new keys during transition). After any incident — rotate immediately.
What is the difference between 401 and 403?
401 Unauthorized means the request lacks valid authentication (no token, expired token, invalid token). 403 Forbidden means the user is authenticated but lacks permission. Use 404 when you want to hide the existence of a resource from unauthorized users.
33. Glossary
- API Gateway
- A server that acts as a single entry point for API calls, enforcing cross-cutting concerns like authentication, rate limiting and TLS termination.
- ABAC
- Attribute-Based Access Control — authorization model that evaluates policies based on attributes of users, resources, actions and environment.
- BOLA
- Broken Object-Level Authorization — vulnerability where an API allows access to resources belonging to other users by manipulating identifiers.
- CORS
- Cross-Origin Resource Sharing — browser security mechanism that controls which origins may make requests to a different domain.
- CSRF
- Cross-Site Request Forgery — an attack that tricks a user's browser into making unintended requests using the user's existing session.
- DAST
- Dynamic Application Security Testing — testing that interacts with a running application to find vulnerabilities.
- HMAC
- Hash-based Message Authentication Code — a mechanism for verifying message integrity and authenticity using a shared secret and a hash function.
- HSTS
- HTTP Strict Transport Security — a response header instructing browsers to only access the site via HTTPS.
- JWT
- JSON Web Token — a compact, URL-safe token format for securely transmitting claims between parties.
- mTLS
- Mutual TLS — a TLS handshake where both client and server present certificates for mutual authentication.
- OAuth 2.0
- An industry-standard authorization framework that enables applications to obtain limited access to user accounts on third-party services.
- OIDC
- OpenID Connect — an identity layer on top of OAuth 2.0 that provides authentication and user identity information.
- PKCE
- Proof Key for Code Exchange — an OAuth extension that prevents authorization code interception attacks, especially for public clients.
- RBAC
- Role-Based Access Control — authorization model where permissions are assigned to roles, and users are assigned to roles.
- SAST
- Static Application Security Testing — source code analysis to detect vulnerabilities without running the application.
- SCA
- Software Composition Analysis — identification of known vulnerabilities in open-source dependencies.
- SSRF
- Server-Side Request Forgery — an attack that makes the server perform requests to unintended destinations, often internal resources.
- WAF
- Web Application Firewall — a security appliance that filters and monitors HTTP traffic between a web application and the internet.
- Zero Trust
- A security model that requires strict identity verification for every person and device trying to access resources, regardless of network location.
34. References & Further Reading
- OWASP API Security Top 10 (2023)
- RFC 6749 — OAuth 2.0 Authorization Framework
- RFC 7519 — JSON Web Token (JWT)
- RFC 7636 — Proof Key for Code Exchange (PKCE)
- RFC 7009 — OAuth 2.0 Token Revocation
- OpenAPI Specification 3.x
- OWASP Cheat Sheet Series
- MDN — Cross-Origin Resource Sharing (CORS)
- CWE/SANS Top 25 Most Dangerous Software Weaknesses
35. Conclusion
Secure API design is not a one-time activity — it is a continuous discipline woven into architecture, development, deployment and operations. The patterns in this guide are not exhaustive, but they address the most common and impactful vulnerabilities you will encounter.
Start by prioritizing based on your threat model:
- Immediate wins — Enable TLS, add authentication to all endpoints, implement schema validation, set security headers.
- High impact — Fix BOLA vulnerabilities, add rate limiting, configure CORS properly, implement structured logging.
- Ongoing — Add security tests to CI, rotate secrets, audit dependencies, review and deprecate old API versions.
Pick one section from this guide and apply it to your API today. Security improves incrementally — every control you add reduces risk.