APIs are the backbone of modern applications, but they are also the #1 attack vector in 2026. According to the OWASP API Security Top 10, broken authentication, excessive data exposure, and injection attacks continue to plague production systems. In this guide, we will walk through the five most critical API security threats and show you exactly how to defend against each one with practical code examples.
1. Broken Object-Level Authorization (BOLA)
BOLA is the most common API vulnerability. It occurs when an API endpoint allows a user to access resources belonging to other users simply by changing an ID in the request.
The Vulnerable Code
// ❌ Bad: No authorization check
app.get("/api/orders/:id", async (req, res) => {
const order = await Order.findById(req.params.id);
res.json(order); // Anyone can access any order!
});The Fix
// ✅ Good: Verify ownership
app.get("/api/orders/:id", authenticate, async (req, res) => {
const order = await Order.findById(req.params.id);
if (!order) return res.status(404).json({ error: "Not found" });
if (order.userId.toString() !== req.user.id) {
return res.status(403).json({ error: "Forbidden" });
}
res.json(order);
});Key principle: Always validate that the authenticated user owns or has permission to access the requested resource. Never rely on obscurity of IDs.
2. Broken Authentication
Weak authentication mechanisms let attackers impersonate legitimate users. In 2026, JWTs remain popular but are frequently misconfigured.
Common JWT Mistakes and Fixes
// ❌ Bad: Using "none" algorithm or weak secrets
const token = jwt.sign(payload, "secret123");
// ✅ Good: Strong secret + explicit algorithm + short expiry
import crypto from "crypto";
const JWT_SECRET = process.env.JWT_SECRET; // 256-bit+ random key
const token = jwt.sign(payload, JWT_SECRET, {
algorithm: "HS256",
expiresIn: "15m", // Short-lived access tokens
issuer: "7tech.co.in",
});
// Verification must pin the algorithm
const decoded = jwt.verify(token, JWT_SECRET, {
algorithms: ["HS256"], // Reject "none" and RS256 confusion
issuer: "7tech.co.in",
});Implement Refresh Token Rotation
app.post("/api/auth/refresh", async (req, res) => {
const { refreshToken } = req.body;
const stored = await RefreshToken.findOne({ token: refreshToken });
if (!stored || stored.revoked || stored.expiresAt < new Date()) {
// If token was already used, revoke entire family (replay attack)
if (stored?.used) await RefreshToken.revokeFamily(stored.familyId);
return res.status(401).json({ error: "Invalid refresh token" });
}
// Mark old token as used and issue new pair
stored.used = true;
await stored.save();
const newAccess = generateAccessToken(stored.userId);
const newRefresh = await RefreshToken.create({
userId: stored.userId,
familyId: stored.familyId,
});
res.json({ accessToken: newAccess, refreshToken: newRefresh.token });
});3. Excessive Data Exposure
APIs often return entire database objects, leaking sensitive fields like passwords, internal IDs, or PII. The fix is simple: always shape your responses.
// ❌ Bad: Returning raw database object
app.get("/api/users/:id", async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user); // Leaks passwordHash, email, SSN, etc.
});
// ✅ Good: Explicit response shaping with a DTO
const sanitizeUser = (user) => ({
id: user.id,
name: user.name,
avatar: user.avatar,
joinedAt: user.createdAt,
});
app.get("/api/users/:id", authenticate, async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ error: "Not found" });
res.json(sanitizeUser(user));
});Pro tip: Use a validation library like zod to define response schemas and strip unknown fields automatically:
import { z } from "zod";
const UserResponse = z.object({
id: z.string(),
name: z.string(),
avatar: z.string().url().nullable(),
});
// In your handler:
res.json(UserResponse.parse(user));4. Rate Limiting and Resource Exhaustion
Without rate limiting, attackers can brute-force credentials, scrape data, or simply crash your server with excessive requests.
Express Rate Limiting with Sliding Window
import rateLimit from "express-rate-limit";
import RedisStore from "rate-limit-redis";
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
// General API rate limit
const apiLimiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
standardHeaders: true,
legacyHeaders: false,
message: { error: "Too many requests, slow down." },
});
// Strict limit for auth endpoints
const authLimiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
skipSuccessfulRequests: true,
});
app.use("/api/", apiLimiter);
app.use("/api/auth/login", authLimiter);5. Injection Attacks (SQL and NoSQL)
Injection remains dangerous in 2026, especially NoSQL injection in MongoDB-based APIs which many developers overlook.
NoSQL Injection Example
// ❌ Vulnerable: Attacker sends { "$gt": "" } as password
app.post("/api/login", async (req, res) => {
const user = await User.findOne({
email: req.body.email,
password: req.body.password // Can be an object!
});
});
// ✅ Safe: Validate and sanitize input
import { z } from "zod";
const LoginSchema = z.object({
email: z.string().email().max(255),
password: z.string().min(8).max(128),
});
app.post("/api/login", async (req, res) => {
const { email, password } = LoginSchema.parse(req.body);
const user = await User.findOne({ email });
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).json({ error: "Invalid credentials" });
}
// Issue tokens...
});Bonus: Security Headers Middleware
Always add security headers to your API responses. Here is a production-ready middleware:
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("Cache-Control", "no-store");
res.setHeader("X-Request-Id", crypto.randomUUID());
next();
});API Security Checklist for 2026
- ✅ Authenticate every request (JWT, OAuth 2.1, API keys)
- ✅ Authorize at the object level — not just the endpoint
- ✅ Validate all input with schemas (Zod, Joi, or similar)
- ✅ Shape all responses — never return raw DB objects
- ✅ Rate limit aggressively, especially auth endpoints
- ✅ Use HTTPS everywhere with HSTS
- ✅ Log all access with request IDs for tracing
- ✅ Rotate secrets and tokens regularly
- ✅ Run automated security scans in CI/CD (OWASP ZAP, Nuclei)
Conclusion
API security is not optional — it is a core engineering responsibility. By addressing these five OWASP threats with proper authorization checks, strong authentication, response shaping, rate limiting, and input validation, you can protect your APIs from the vast majority of real-world attacks. Start with the checklist above and integrate these patterns into every API you build in 2026.

Leave a Reply