Gartner predicted API vulnerabilities would become the most common attack vector by 2022. By 2025, that prediction has materialized — API attacks drive 34% of all web application security incidents. Most of these attacks exploit well-documented, preventable vulnerabilities.
The OWASP API Security Top 10 (2023 Edition)
OWASP's API-specific vulnerability list is the starting point for any API security program:
| Rank | Vulnerability | Brief |
|---|---|---|
| API1 | Broken Object Level Authorization | Access to other users' objects |
| API2 | Broken Authentication | Weak auth mechanisms |
| API3 | Broken Object Property Level Authorization | Mass assignment, excessive exposure |
| API4 | Unrestricted Resource Consumption | No rate limiting |
| API5 | Broken Function Level Authorization | Admin functions exposed |
| API6 | Unrestricted Access to Sensitive Business Flows | Scraping, abuse of critical flows |
| API7 | Server Side Request Forgery | SSRF attacks |
| API8 | Security Misconfiguration | Missing headers, verbose errors |
| API9 | Improper Inventory Management | Shadow APIs, deprecated endpoints |
| API10 | Unsafe Consumption of APIs | Trusting external API responses |
Implementation Checklist
Authentication
OAuth 2.0 / JWT:
// Verify JWT on every request
import jwt from 'jsonwebtoken';
function authenticateRequest(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'No token provided' });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'], // Specify allowed algorithms explicitly
expiresIn: '24h'
});
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
}
API Key rotation:
- Rotate API keys every 90 days (or on suspected compromise)
- Never embed API keys in client-side code
- Use environment variables, not config files committed to git
Authorization (BOLA Prevention — Most Common Vulnerability)
The #1 API vulnerability: users accessing other users' data via object ID manipulation.
Wrong (vulnerable):
// Anyone can access any user's data by changing the ID
app.get('/api/users/:id/data', authenticate, async (req, res) => {
const data = await getData(req.params.id);
res.json(data);
});
Correct:
// Verify the authenticated user owns the requested resource
app.get('/api/users/:id/data', authenticate, async (req, res) => {
if (req.user.id !== req.params.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' });
}
const data = await getData(req.params.id);
res.json(data);
});
Rate Limiting (API4)
Essential for all APIs — prevents abuse, DoS, and scraping:
import rateLimit from 'express-rate-limit';
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => req.user?.id || req.ip, // Per-user, not per-IP
});
// Stricter for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
message: 'Too many auth attempts'
});
app.use('/api/', apiLimiter);
app.use('/api/auth/', authLimiter);
Input Validation
Validate all input — don't trust client-submitted data:
import { z } from 'zod';
const createUserSchema = z.object({
email: z.string().email().max(255),
name: z.string().min(1).max(100),
role: z.enum(['user', 'editor']), // Never accept 'admin' from client
});
app.post('/api/users', authenticate, async (req, res) => {
const parsed = createUserSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: parsed.error.format() });
}
// Use parsed.data, never req.body directly
});
Security Headers
Every API response should include:
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
res.setHeader('Content-Security-Policy', "default-src 'none'");
res.removeHeader('X-Powered-By'); // Don't leak framework info
next();
});
Error Handling (Don't Leak Information)
Wrong (leaks stack trace, framework info, DB schema):
app.get('/api/data', async (req, res) => {
const data = await db.query(sql);
res.json(data);
// On error: sends full stack trace to client
});
Correct:
app.get('/api/data', async (req, res) => {
try {
const data = await db.query(sql);
res.json(data);
} catch (err) {
console.error('DB error:', err); // Log full error server-side
res.status(500).json({ error: 'Internal server error' }); // Generic message to client
}
});
API Security Testing
Before shipping, test with:
| Tool | Purpose | Cost |
|---|---|---|
| OWASP ZAP | Active vulnerability scanning | Free |
| Postman (Security) | API security test collections | Free/paid |
| Burp Suite | Professional pen testing | $449/year |
| Semgrep | Static analysis, secrets scanning | Free (OSS) |
| Snyk | Dependency vulnerability scanning | Free tier available |
Automated testing catches the most common vulnerabilities. Manual penetration testing should be done before major launches.
Use the Cloud Cost Calculator to estimate the infrastructure cost of adding WAF (Web Application Firewall) protection to your APIs.