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 SQLRate 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.Node.js-specific attacks: prototype pollution, ReDoS and SSRF
Three attacks deserve extra attention in Node.js APIs, because they exploit how JavaScript and the event loop work.
Prototype pollution
If user-controlled JSON reaches a recursive merge or a set(obj, path, value) helper, a key like __proto__ can write to Object.prototype and change the behavior of every object in the process. Even a shallow Object.assign({}, body) with a parsed __proto__ key replaces the target’s prototype, so settings.isAdmin suddenly reads true. Validate the request body against a schema that allows only known keys, merge into null-prototype objects, skip the dangerous keys, and keep merge libraries patched.
ReDoS
Regular expressions with nested quantifiers, such as /^(a+)+$/, can backtrack exponentially on crafted input. Because JavaScript runs on one thread, one slow match blocks every request. Never compile user-supplied patterns, review regexes that run on user input (tools such as safe-regex flag risky patterns), cap input length, or use a linear-time engine such as RE2 through the re2 package.
SSRF
When your server fetches a URL that a user supplied (webhooks, link previews, imports), an attacker can point it at internal services or the cloud metadata endpoint. Checking the hostname string isn’t enough: localhost, decimal IPs and DNS names that resolve to private addresses all get past a naive filter. Resolve the name and check every resolved address against a block list, as below, then connect to the address you checked and re-check after each redirect, or disable redirects.
import { lookup } from "node:dns/promises";
import { BlockList, isIP } from "node:net";
// 1. Prototype pollution: copy only own, safe keys into a null-prototype object.
const FORBIDDEN = new Set(["__proto__", "constructor", "prototype"]);
function safeMerge(target, source) {
for (const [key, value] of Object.entries(source)) {
if (FORBIDDEN.has(key)) continue;
if (value && typeof value === "object" && !Array.isArray(value)) {
target[key] = safeMerge(target[key] ?? Object.create(null), value);
} else {
target[key] = value;
}
}
return target;
}
const body = JSON.parse('{"theme":"dark","__proto__":{"isAdmin":true}}');
const settings = safeMerge(Object.create(null), body);
console.log(settings.theme, settings.isAdmin, ({}).isAdmin); // dark undefined undefined
// 2. SSRF: check every resolved address, not the hostname string.
const blocked = new BlockList();
for (const [net, prefix] of [["0.0.0.0", 8], ["10.0.0.0", 8], ["100.64.0.0", 10], ["127.0.0.0", 8],
["169.254.0.0", 16], ["172.16.0.0", 12], ["192.168.0.0", 16]]) {
blocked.addSubnet(net, prefix, "ipv4");
}
for (const [net, prefix] of [["::1", 128], ["fc00::", 7], ["fe80::", 10]]) {
blocked.addSubnet(net, prefix, "ipv6");
}
async function assertPublicUrl(raw) {
const url = new URL(raw);
if (!["http:", "https:"].includes(url.protocol)) throw new Error("only http(s) allowed");
const host = url.hostname.replace(/^\[|\]$/g, "");
const addrs = isIP(host) ? [{ address: host, family: isIP(host) }]
: await lookup(host, { all: true });
for (const { address, family } of addrs) {
const mapped = address.toLowerCase().startsWith("::ffff:") ? address.slice(7) : address;
const type = isIP(mapped) === 4 ? "ipv4" : "ipv6";
if (blocked.check(mapped, type)) throw new Error(`blocked address ${address}`);
}
return { url, addrs }; // connect to one of these exact addresses, and re-check after redirects
}
for (const u of ["http://127.0.0.1/admin", "http://169.254.169.254/latest/meta-data/", "http://localhost:8080/", "file:///etc/passwd"]) {
try { await assertPublicUrl(u); console.log("allowed", u); }
catch (e) { console.log("rejected", u, "-", e.message); }
}Run with Node.js 20, it prints dark undefined undefined for the merge, then rejects all four URLs, including localhost, which resolved to ::1. On AWS, also require IMDSv2 on instances so a forwarded request can’t read credentials from the metadata service. See the OWASP SSRF prevention cheat sheet and the prototype pollution cheat sheet.
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.
Free Weekly Newsletter
🚀 Don’t Miss the Next Cheat Code
Related: Node.js Security Best Practices: Protect Your API From the Most Common Attacks · Express.js Best Practices: Build Production-Ready APIs With Node.js · Day 68: Abuse and Cost Attacks
Related: HTTP Status Codes Explained: Every Code With Real-World Examples and When to Use Each · GraphQL vs REST: A Practical Comparison for Production APIs in 2025 · Docker Best Practices: Build Smaller, Faster, More Secure Images in Production
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- API Debugging in 30 Seconds: The Fastest Ways to Fix API Issues
Newly added
- n8n vs AI Agent: I Gave Both the Same Job (Local, Ollama)
- Local AI Video Generator: Build Your Own App (Free, Python, No GPU)
- AI Wrote Raw Binary Instead of Code. Here’s What Happened (WebAssembly)
- I Built a Browser That Only Runs Encrypted Binary (Ep 1)
- RAG From Scratch: Chunking, Embeddings, Reranking and Evaluation (Real Numbers)
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
