API Security Complete Guide 2026: OWASP Top 10, JWT, CORS, and Rate Limiting

Sanjeev SharmaSanjeev Sharma
6 min read

Advertisement

API Security 2026: The Checklist That Prevents Breaches

Every production API gets attacked. Most breaches are preventable with basic security practices. This guide covers everything you need.

OWASP Top 10 API Vulnerabilities 2026

  1. Broken Object Level Authorization — User can access other users' data
  2. Broken Authentication — Weak tokens, no brute force protection
  3. Broken Object Property Level Authorization — Returns sensitive fields
  4. Unrestricted Resource Consumption — No rate limiting
  5. Broken Function Level Authorization — Non-admin can call admin endpoints
  6. Server-Side Request Forgery (SSRF) — API fetches internal URLs
  7. Security Misconfiguration — Verbose errors, open CORS
  8. Lack of Input Validation — Injection, oversized payloads
  9. Improper Asset Management — Old API versions still running
  10. Unsafe Consumption of APIs — Trusting external API responses blindly

Input Validation and Sanitization

import { z } from 'zod'
import { Request, Response, NextFunction } from 'express'

// Strict input validation with Zod
const CreateUserSchema = z.object({
  email: z.string().email().max(255),
  password: z.string()
    .min(8, 'Password too short')
    .max(100, 'Password too long')
    .regex(/[A-Z]/, 'Must contain uppercase')
    .regex(/[0-9]/, 'Must contain number'),
  name: z.string().min(1).max(100).trim(),
  role: z.enum(['USER', 'MODERATOR']),  // Never accept 'ADMIN' from user
  age: z.number().int().min(13).max(120).optional(),
})

function validate(schema: z.ZodSchema) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.body)

    if (!result.success) {
      return res.status(400).json({
        error: 'Validation failed',
        details: result.error.flatten().fieldErrors,
      })
    }

    req.body = result.data  // Use sanitized data
    next()
  }
}

// Prevent mass assignment — only allow safe fields
app.post('/api/users', validate(CreateUserSchema), async (req, res) => {
  const { email, password, name } = req.body  // role is NOT passed to DB
  const user = await userService.create({ email, password, name, role: 'USER' })
  res.status(201).json(user)
})

SQL Injection Prevention

// ALWAYS use parameterized queries
// NEVER use string concatenation

// ❌ DANGEROUS:
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`
// Attacker sends: ' OR '1'='1

// ✓ SAFE with Prisma:
const user = await prisma.user.findUnique({
  where: { email: req.body.email },
})

// ✓ SAFE with pg:
const result = await pool.query(
  'SELECT * FROM users WHERE email = $1',
  [req.body.email]  // Parameterized
)

// ✓ SAFE raw SQL in Prisma:
const users = await prisma.$queryRaw`
  SELECT * FROM users WHERE email = ${req.body.email}
`
// Template literals are auto-escaped in Prisma

JWT Security Best Practices

import jwt from 'jsonwebtoken'
import crypto from 'crypto'

// ✓ Use strong secrets (32+ bytes)
const JWT_SECRET = process.env.JWT_SECRET  // Must be 256+ bits random
// Generate: openssl rand -hex 32

// ✓ Short expiry for access tokens
function signAccessToken(userId: string): string {
  return jwt.sign(
    { sub: userId, type: 'access', jti: crypto.randomUUID() },
    JWT_SECRET!,
    { expiresIn: '15m', algorithm: 'HS256' }
  )
}

// ✓ Validate on every request
function verifyToken(token: string): JwtPayload {
  try {
    return jwt.verify(token, JWT_SECRET!) as JwtPayload
  } catch (err) {
    if (err instanceof jwt.TokenExpiredError) throw new Error('Token expired')
    if (err instanceof jwt.JsonWebTokenError) throw new Error('Invalid token')
    throw err
  }
}

// ✓ Token rotation to detect theft
class RefreshTokenService {
  async rotate(oldToken: string): Promise<{ access: string; refresh: string }> {
    const payload = verifyToken(oldToken)

    // Check if token was already used (replay attack detection)
    const used = await redis.get(`used_refresh:${payload.jti}`)
    if (used) {
      // Someone is replaying a token — invalidate all sessions for this user
      await this.invalidateAllSessions(payload.sub)
      throw new Error('Token reuse detected')
    }

    // Mark as used
    await redis.setex(`used_refresh:${payload.jti}`, 7 * 24 * 3600, '1')

    return {
      access: signAccessToken(payload.sub),
      refresh: signRefreshToken(payload.sub),
    }
  }
}

CORS Configuration

import cors from 'cors'

const allowedOrigins = [
  'https://webcoderspeed.com',
  'https://www.webcoderspeed.com',
  ...(process.env.NODE_ENV === 'development' ? ['http://localhost:3000'] : []),
]

app.use(cors({
  origin: (origin, callback) => {
    // Allow requests with no origin (mobile apps, curl)
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true)
    } else {
      callback(new Error(`CORS policy violation: ${origin}`))
    }
  },
  credentials: true,        // Allow cookies
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  exposedHeaders: ['X-RateLimit-Remaining'],
  maxAge: 86400,           // Cache preflight for 24h
}))

Security Headers

import helmet from 'helmet'

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'nonce-{NONCE}'"],  // Use nonces, not unsafe-inline
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", 'data:', 'https://cdn.example.com'],
      connectSrc: ["'self'", 'https://api.example.com'],
      fontSrc: ["'self'", 'https://fonts.gstatic.com'],
      objectSrc: ["'none'"],
      upgradeInsecureRequests: [],
    },
  },
  crossOriginEmbedderPolicy: true,
  crossOriginResourcePolicy: { policy: 'same-origin' },
  dnsPrefetchControl: { allow: false },
  frameguard: { action: 'deny' },      // X-Frame-Options: DENY
  hsts: { maxAge: 63072000, includeSubDomains: true, preload: true },
  noSniff: true,                        // X-Content-Type-Options: nosniff
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
  xssFilter: true,
}))

Broken Object Level Authorization (BOLA)

// ❌ VULNERABLE: User can access any post by ID
app.get('/api/posts/:id', authenticate, async (req, res) => {
  const post = await prisma.post.findUnique({ where: { id: req.params.id } })
  return res.json(post)  // Returns ANY user's draft posts!
})

// ✓ FIXED: Always scope to authenticated user
app.get('/api/posts/:id', authenticate, async (req, res) => {
  const post = await prisma.post.findUnique({
    where: { id: req.params.id },
  })

  if (!post) return res.status(404).json({ error: 'Not found' })

  // Check authorization
  if (!post.published && post.authorId !== req.user.id) {
    // Non-published posts only visible to author
    return res.status(403).json({ error: 'Forbidden' })
  }

  return res.json(post)
})

Prevent Sensitive Data Exposure

// ✓ Never expose sensitive fields
const SafeUserSchema = z.object({
  id: z.string(),
  email: z.string(),
  name: z.string(),
  role: z.string(),
  createdAt: z.date(),
  // NOT: passwordHash, refreshTokens, internalNotes
})

function sanitizeUser(user: User): SafeUser {
  return SafeUserSchema.parse(user)
  // Extra fields are automatically stripped
}

// ✓ Use select in Prisma
const user = await prisma.user.findUnique({
  where: { id },
  select: {
    id: true, email: true, name: true, role: true,
    // passwordHash: false — never expose!
  },
})

Security Checklist

CheckStatus
HTTPS everywhere
Parameterized queries
Input validation (Zod)
Output sanitization
JWT short expiry + rotation
CORS whitelist
Security headers (Helmet)
Rate limiting
BOLA checks on every endpoint
Audit logs
Dependency scanning (npm audit)

Run npm audit weekly. Enable Dependabot. A single vulnerable dependency caused 73% of supply chain attacks in 2025.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro