API Security in 2026: How to Protect Your REST APIs from the Top 5 OWASP Threats

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.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Privacy Policy · Contact · Sitemap

© 7Tech – Programming and Tech Tutorials