APIs are the backbone of modern applications, but they are also the number one attack vector in 2026. With the OWASP API Security Top 10 updated and threat actors becoming more sophisticated, securing your REST APIs is no longer optional — it is a fundamental part of development. In this guide, we will walk through the top 5 API security threats and show you exactly how to defend against each one with real, production-ready code.
1. Broken Object-Level Authorization (BOLA)
BOLA remains the #1 API vulnerability. It occurs when an API endpoint exposes object IDs and fails to verify that the requesting user actually owns or has access to that object.
The Vulnerable Code
// ❌ BAD — No ownership check
app.get("/api/orders/:id", async (req, res) => {
const order = await Order.findById(req.params.id);
res.json(order); // Any authenticated user can access ANY order
});The Fix
// ✅ GOOD — Verify ownership
app.get("/api/orders/:id", authenticate, async (req, res) => {
const order = await Order.findOne({
_id: req.params.id,
userId: req.user.id // Only return if the user owns it
});
if (!order) return res.status(404).json({ error: "Not found" });
res.json(order);
});Key takeaway: Never trust the client. Always filter database queries by the authenticated user’s identity. Use middleware to enforce this consistently across all endpoints.
2. Broken Authentication
Weak authentication lets attackers impersonate users, steal sessions, or brute-force credentials. In 2026, JWTs are everywhere — but most teams misconfigure them.
Secure JWT Implementation
import jwt from "jsonwebtoken";
import { rateLimit } from "express-rate-limit";
// Short-lived access tokens + refresh token rotation
function generateTokens(user) {
const accessToken = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET,
{ algorithm: "HS256", expiresIn: "15m" } // Short-lived!
);
const refreshToken = jwt.sign(
{ sub: user.id, jti: crypto.randomUUID() },
process.env.REFRESH_SECRET,
{ expiresIn: "7d" }
);
return { accessToken, refreshToken };
}
// Rate-limit login attempts
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per window
message: { error: "Too many login attempts. Try again later." }
});
app.post("/api/auth/login", loginLimiter, async (req, res) => {
// ... validate credentials
});Checklist:
- Use short-lived access tokens (15 minutes or less)
- Implement refresh token rotation and revocation
- Rate-limit authentication endpoints aggressively
- Never store JWTs in localStorage — use httpOnly cookies
- Always validate the
algheader to prevent “none” algorithm attacks
3. Unrestricted Resource Consumption
Without proper limits, a single client can overwhelm your API with massive payloads, deep queries, or unlimited pagination — leading to denial of service or sky-high cloud bills.
Defense in Depth
import express from "express";
import { rateLimit } from "express-rate-limit";
const app = express();
// 1. Limit request body size
app.use(express.json({ limit: "100kb" }));
// 2. Global rate limiting
app.use(rateLimit({
windowMs: 60 * 1000,
max: 100, // 100 requests per minute
standardHeaders: true,
legacyHeaders: false,
}));
// 3. Pagination guard
function paginationGuard(req, res, next) {
const limit = Math.min(parseInt(req.query.limit) || 20, 100);
const page = Math.max(parseInt(req.query.page) || 1, 1);
req.pagination = { limit, page, skip: (page - 1) * limit };
next();
}
app.get("/api/products", paginationGuard, async (req, res) => {
const products = await Product.find()
.skip(req.pagination.skip)
.limit(req.pagination.limit);
res.json(products);
});For GraphQL APIs, also enforce query depth limiting and complexity analysis to prevent deeply nested queries from consuming excessive resources.
4. Server-Side Request Forgery (SSRF)
SSRF happens when your API fetches a URL provided by the user without validation. Attackers exploit this to access internal services, cloud metadata endpoints, or private networks.
Safe URL Fetching
import { URL } from "url";
import dns from "dns/promises";
import net from "net";
async function safeFetch(userUrl) {
const parsed = new URL(userUrl);
// Block non-HTTP protocols
if (!["http:", "https:"].includes(parsed.protocol)) {
throw new Error("Only HTTP(S) allowed");
}
// Resolve DNS and block private IPs
const { address } = await dns.lookup(parsed.hostname);
if (net.isIP(address)) {
const parts = address.split(".").map(Number);
const isPrivate =
parts[0] === 10 ||
parts[0] === 127 ||
(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) ||
(parts[0] === 192 && parts[1] === 168) ||
address === "169.254.169.254"; // AWS metadata
if (isPrivate) throw new Error("Access to internal addresses is blocked");
}
return fetch(userUrl, { redirect: "error", signal: AbortSignal.timeout(5000) });
}
app.post("/api/webhook/test", authenticate, async (req, res) => {
try {
const response = await safeFetch(req.body.url);
res.json({ status: response.status });
} catch (err) {
res.status(400).json({ error: "Invalid or blocked URL" });
}
});Critical: Always block the cloud metadata IP 169.254.169.254. In 2019, the Capital One breach exploited exactly this SSRF vector to steal 100 million customer records from AWS.
5. Security Misconfiguration
Misconfigured CORS, verbose error messages, missing security headers, and exposed debug endpoints are low-hanging fruit for attackers.
Production Security Middleware
import helmet from "helmet";
import cors from "cors";
// Security headers
app.use(helmet());
// Strict CORS — only allow your frontend
app.use(cors({
origin: ["https://yourapp.com"],
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
maxAge: 86400
}));
// Strip sensitive headers
app.disable("x-powered-by");
// Production error handler — NEVER leak stack traces
app.use((err, req, res, next) => {
console.error(err); // Log internally
res.status(err.status || 500).json({
error: process.env.NODE_ENV === "production"
? "Internal server error" // Generic message for clients
: err.message // Detailed only in dev
});
});Deployment checklist:
- Use
helmet()to set all recommended security headers automatically - Lock CORS to your exact frontend origin(s) — never use
*with credentials - Disable stack traces, debug routes, and verbose errors in production
- Audit your
/headerswith securityheaders.com - Run
npm auditin CI/CD to catch vulnerable dependencies
Bonus: Security Testing Automation
Add this script to your CI pipeline to catch common issues before they reach production:
# Install OWASP ZAP CLI
npm install -g @zaproxy/cli
# Quick API scan in CI
zap-cli quick-scan \
--self-contained \
--start-options "-config api.disablekey=true" \
-o "-config scanner.strength=HIGH" \
https://staging-api.yourapp.com/openapi.jsonSummary
API security in 2026 comes down to five core practices:
- Always verify ownership — filter every query by the authenticated user
- Lock down authentication — short tokens, rate limiting, httpOnly cookies
- Limit everything — body size, rate, pagination, query complexity
- Validate all URLs — block private IPs and cloud metadata endpoints
- Harden your config — helmet, strict CORS, no stack traces in production
Security is not a feature you ship once — it is a practice you build into every endpoint. Start with these five fixes today, and your APIs will be significantly harder to exploit.

Leave a Reply