APIs are the backbone of modern applications — powering mobile apps, SPAs, microservices, and third-party integrations. They're also the #1 attack vector in 2026. According to Gartner, API attacks have increased 300% since 2022, and by 2026, APIs will be the most-frequent attack vector for enterprise web applications.
The OWASP API Security Top 10 identifies the most critical API risks. This guide covers each risk with practical code examples and fix commands for Node.js, Python, PHP, and Java.
Also see: How to Fix CORS Misconfiguration, CORS Policy Examples by Framework, API Rate Limiting Guide, and SQL Injection Testing Checklist.
Why API Security Is Critical
- APIs expose business logic directly — unlike web pages, APIs provide structured access to data and operations
- APIs are discoverable — Swagger docs, predictable endpoints, and API directories make them easy to find
- APIs handle sensitive data — authentication tokens, personal data, payment information
- APIs are automated targets — bots can probe thousands of endpoints per second
OWASP API Security Top 10 (2023)
- API1 — Broken Object Level Authorization (BOLA): Users can access other users' data by changing IDs in requests
- API2 — Broken Authentication: Weak auth mechanisms, missing token validation
- API3 — Broken Object Property Level Authorization: APIs expose more data fields than necessary
- API4 — Unrestricted Resource Consumption: No rate limiting, allowing DoS and brute-force
- API5 — Broken Function Level Authorization: Users can access admin functions
- API6 — Unrestricted Access to Sensitive Business Flows: Automated abuse of business logic
- API7 — Server-Side Request Forgery (SSRF): API fetches attacker-controlled URLs
- API8 — Security Misconfiguration: Default configs, verbose errors, missing headers
- API9 — Improper Inventory Management: Forgotten old API versions, undocumented endpoints
- API10 — Unsafe Consumption of APIs: Trusting third-party API responses without validation
Authentication & Authorization
JWT Best Practices
// Node.js — Secure JWT implementation
const jwt = require('jsonwebtoken');
// Sign with RS256 (asymmetric) instead of HS256
const token = jwt.sign(
{ userId: user.id, role: user.role },
privateKey,
{
algorithm: 'RS256',
expiresIn: '15m', // Short-lived access tokens
issuer: 'api.example.com',
audience: 'app.example.com'
}
);
// Verify with all checks
function verifyToken(token) {
return jwt.verify(token, publicKey, {
algorithms: ['RS256'], // Prevent algorithm confusion
issuer: 'api.example.com',
audience: 'app.example.com',
clockTolerance: 30 // 30 second clock skew tolerance
});
}
// Middleware
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing authorization header' });
}
try {
req.user = verifyToken(authHeader.split(' ')[1]);
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
Authorization: Prevent BOLA (API1)
// VULNERABLE — No ownership check
app.get('/api/orders/:id', auth, async (req, res) => {
const order = await Order.findById(req.params.id);
res.json(order); // Any authenticated user can view any order!
});
// SECURE — Check ownership
app.get('/api/orders/:id', auth, async (req, res) => {
const order = await Order.findOne({
_id: req.params.id,
userId: req.user.id // Only return if user owns this order
});
if (!order) return res.status(404).json({ error: 'Not found' });
res.json(order);
});
Rate Limiting & Throttling
// Express.js — Rate limiting with express-rate-limit
const rateLimit = require('express-rate-limit');
// General API rate limit
const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute window
max: 100, // 100 requests per minute
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false,
message: { error: 'Too many requests, try again later' }
});
// Strict limit for authentication endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per 15 minutes
message: { error: 'Too many login attempts' }
});
app.use('/api/', apiLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);
# Nginx — Rate limiting
http {
# Define rate limit zones
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m;
limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m;
server {
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_req_status 429;
}
location /api/auth/ {
limit_req zone=auth burst=3 nodelay;
limit_req_status 429;
}
}
}
Input Validation
// Express.js — Input validation with Joi
const Joi = require('joi');
const createUserSchema = Joi.object({
email: Joi.string().email().required().max(255),
password: Joi.string().min(12).max(128).required(),
name: Joi.string().alphanum().min(2).max(50).required(),
role: Joi.forbidden() // Prevent role escalation
});
app.post('/api/users', auth, async (req, res) => {
const { error, value } = createUserSchema.validate(req.body, {
abortEarly: false,
stripUnknown: true // Remove unexpected fields
});
if (error) {
return res.status(400).json({
error: 'Validation failed',
details: error.details.map(d => d.message)
});
}
// Use validated 'value', not raw req.body
const user = await User.create(value);
res.status(201).json(user);
});
CORS Configuration
See our detailed CORS Misconfiguration Fix Guide for complete configurations. Key rules:
- Never use
Access-Control-Allow-Origin: *with credentials - Whitelist specific origins — don't reflect the Origin header blindly
- Handle preflight (OPTIONS) requests explicitly
- Set
Access-Control-Max-Ageto cache preflight responses
API Key Management
# API Key Best Practices:
✅ Generate cryptographically random keys (min 32 bytes)
✅ Hash keys before storing in database (bcrypt or SHA-256)
✅ Transmit keys in headers, NEVER in URL query parameters
✅ Implement key rotation with grace periods
✅ Set per-key rate limits and permissions
✅ Log all key usage for auditing
✅ Provide key revocation mechanism
✅ Use separate keys for development and production
# Generate a secure API key
openssl rand -hex 32
# Output: a1b2c3d4e5f6...64 character hex string
# Node.js — Generate API key
const crypto = require('crypto');
const apiKey = crypto.randomBytes(32).toString('hex');
Logging & Monitoring
// Express.js — API request logging
const morgan = require('morgan');
// Custom format with security-relevant fields
morgan.token('user-id', (req) => req.user?.id || 'anonymous');
morgan.token('api-key', (req) => {
const key = req.headers['x-api-key'];
return key ? key.substring(0, 8) + '...' : 'none';
});
app.use(morgan(':date[iso] :method :url :status :response-time ms user=:user-id key=:api-key'));
// Log security events
function logSecurityEvent(event, details) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
event,
...details
}));
}
// Examples:
logSecurityEvent('auth_failure', { ip: req.ip, username: req.body.username });
logSecurityEvent('rate_limit_exceeded', { ip: req.ip, endpoint: req.path });
logSecurityEvent('unauthorized_access', { ip: req.ip, userId: req.user?.id, resource: req.path });
GraphQL-Specific Security
// GraphQL security measures
// 1. Disable introspection in production
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production',
playground: false
});
// 2. Query depth limiting
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
validationRules: [depthLimit(5)] // Max 5 levels deep
});
// 3. Query complexity limiting
const { createComplexityLimitRule } = require('graphql-validation-complexity');
const server = new ApolloServer({
validationRules: [createComplexityLimitRule(1000)]
});
// 4. Rate limiting per query
// Use the same rate limiting as REST APIs
API Security Checklist
- ☐ HTTPS only (no HTTP endpoints)
- ☐ Authentication on all endpoints (JWT/OAuth 2.0)
- ☐ Authorization checks on every request (BOLA prevention)
- ☐ Rate limiting on all endpoints
- ☐ Strict rate limiting on auth endpoints
- ☐ Input validation with schema (Joi, Zod, Pydantic)
- ☐ CORS configured with specific origins
- ☐ API keys hashed in database
- ☐ No sensitive data in URLs
- ☐ Error messages don't leak internals
- ☐ Swagger/OpenAPI docs not public in production
- ☐ GraphQL introspection disabled in production
- ☐ Request/response logging enabled
- ☐ Old API versions decommissioned
Scan Your API Security
Free API & CORS security scan — checks endpoints, CORS, authentication, and Swagger exposure.
Scan Your API NowFrequently Asked Questions
What are the top API security risks?
The OWASP API Security Top 10 lists Broken Object Level Authorization (BOLA), Broken Authentication, and Unrestricted Resource Consumption as the top three risks.
How should I authenticate API requests?
Use OAuth 2.0 with short-lived JWT access tokens for user-facing APIs. Use API keys with HMAC signatures for server-to-server communication. Always use HTTPS.
How do I implement API rate limiting?
Use a sliding window or token bucket algorithm. Set limits per API key, per IP, and per user. Return 429 with Retry-After header. Common: 100 req/min authenticated, 20/min unauthenticated.