APIs are the number one attack surface for modern web applications. OWASP’s API Security Top 10 describes vulnerabilities that attackers exploit daily — broken authentication, excessive data exposure, mass assignment, and injection attacks. This guide covers each threat with the actual code-level defenses that prevent them, not just theory.
⚡ TL;DR: Validate every input server-side. Use short-lived JWTs with refresh tokens. Implement rate limiting on every endpoint. Return minimum data (never expose internal fields). Use parameterized queries — never string concatenation. Set CORS to specific origins. Log auth failures. Never trust client-provided IDs for authorization.
OWASP API Top 10 — the real threats
// 1. Broken Object Level Authorization (BOLA/IDOR) — most common API flaw
// Attacker changes ID in URL to access other users' data
// VULNERABLE:
app.get('/invoices/:id', auth, async (req, res) => {
const invoice = await db.getInvoice(req.params.id);
res.json(invoice); // Returns invoice regardless of who owns it!
});
// Attacker: GET /invoices/12345 (not their invoice) → gets it!
// SECURE:
app.get('/invoices/:id', auth, async (req, res) => {
const invoice = await db.query(
'SELECT * FROM invoices WHERE id = $1 AND user_id = $2',
[req.params.id, req.user.id] // ALWAYS scope to authenticated user
);
if (!invoice) return res.status(404).json({ error: 'Not found' });
res.json(invoice);
});
// 2. Broken Authentication
// VULNERABLE: no token expiry, weak secret, algorithm=none
const token = jwt.sign(payload, 'weak');
// SECURE:
const token = jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: '15m', // Short-lived
algorithm: 'HS256' // Always specify algorithm explicitly
});
// ALWAYS verify: jwt.verify(token, secret, { algorithms: ['HS256'] })
Input validation and injection prevention
import { z } from 'zod';
// Validate EVERYTHING from the client — never trust request data
const CreateUserSchema = z.object({
name: z.string().min(1).max(100).trim(),
email: z.string().email().toLowerCase(),
age: z.number().int().min(18).max(120),
role: z.enum(['user', 'moderator']).default('user'),
// NEVER accept: 'admin', isAdmin: true, userId, internalId, etc.
});
app.post('/users', async (req, res) => {
const parsed = CreateUserSchema.safeParse(req.body);
if (!parsed.success) return res.status(422).json({ error: parsed.error.format() });
const { name, email, age, role } = parsed.data; // Only allowed fields
await db.createUser({ name, email, age, role }); // Mass assignment prevented
});
// SQL injection prevention — ALWAYS parameterized queries
// VULNERABLE:
const user = await db.query(`SELECT * FROM users WHERE email = '${email}'`);
// Attacker: email = "x' OR 1=1 --" → returns all users!
// SECURE:
const user = await db.query('SELECT * FROM users WHERE email = $1', [email]);
// $1 is a parameter — never concatenated into SQL
Rate limiting and DDoS protection
import rateLimit from 'express-rate-limit';
// Different limits for different endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // 10 attempts per IP per 15 min
standardHeaders: true, // Return rate limit headers
message: { error: 'Too many attempts, try again in 15 minutes' },
});
const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
});
// Apply different limits to different routes
app.use('/auth/', authLimiter); // Stricter: prevent brute force
app.use('/api/', apiLimiter); // Standard: prevent abuse
// Also limit by API key (not just IP):
const keyLimiter = rateLimit({
keyGenerator: (req) => req.headers['x-api-key'] || req.ip,
max: 1000, // Per API key per minute
});
CORS and security headers
import cors from 'cors';
import helmet from 'helmet';
// WRONG: allow all origins
app.use(cors()); // Access-Control-Allow-Origin: *
// Anyone can call your API with a browser request from any domain!
// CORRECT: specific allowed origins
const allowedOrigins = [
'https://myapp.com',
'https://admin.myapp.com',
];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) callback(null, true);
else callback(new Error('CORS: Origin not allowed'));
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
}));
// Helmet: sets secure HTTP headers automatically
app.use(helmet()); // Sets: X-Content-Type-Options, X-Frame-Options,
// Content-Security-Policy, Strict-Transport-Security, etc.
API security checklist
- ✅ BOLA: always scope DB queries to authenticated user’s ID
- ✅ Validate all inputs with schema validation (Zod, Joi, Yup)
- ✅ Parameterized queries — never string-concatenated SQL
- ✅ Short-lived JWTs (15m access + 7d refresh) with explicit algorithm
- ✅ Rate limiting on auth endpoints (10 req/15min) and all API endpoints
- ✅ CORS: specific origins, not wildcard
- ✅ Helmet.js for security headers
- ✅ Log auth failures, flag suspicious patterns
- ❌ Never trust client-provided user IDs, roles, or permissions
- ❌ Never return stack traces, SQL queries, or internal paths in errors
API security connects to the REST API design guide — security decisions should be made at design time, not retrofitted. The AWS S3 presigned URL guide shows how authentication works at the infrastructure layer. External reference: OWASP API Security Top 10.
Recommended Reading
→ Designing Data-Intensive Applications — The essential book every senior developer needs. Covers distributed systems, databases, and production architecture.
→ The Pragmatic Programmer — Timeless engineering wisdom for writing better, more maintainable code at any level.
Affiliate links. We earn a small commission at no extra cost to you.
Free Weekly Newsletter
🚀 Don’t Miss the Next Cheat Code
Join 1,000+ senior developers getting expert-level JS, Python, AWS, system design and AI secrets every week. Zero fluff, pure signal.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
