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

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 alg header 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 /headers with securityheaders.com
  • Run npm audit in 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.json

Summary

API security in 2026 comes down to five core practices:

  1. Always verify ownership — filter every query by the authenticated user
  2. Lock down authentication — short tokens, rate limiting, httpOnly cookies
  3. Limit everything — body size, rate, pagination, query complexity
  4. Validate all URLs — block private IPs and cloud metadata endpoints
  5. 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.

Comments

Leave a Reply

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

Privacy Policy · Contact · Sitemap

© 7Tech – Programming and Tech Tutorials