API Security Complete Guide 2026: OWASP Top 10, JWT, CORS, and Rate Limiting
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
- Input Validation and Sanitization
- SQL Injection Prevention
- JWT Security Best Practices
- CORS Configuration
- Security Headers
- Broken Object Level Authorization (BOLA)
- Prevent Sensitive Data Exposure
- Security Checklist
OWASP Top 10 API Vulnerabilities 2026
- Broken Object Level Authorization — User can access other users' data
- Broken Authentication — Weak tokens, no brute force protection
- Broken Object Property Level Authorization — Returns sensitive fields
- Unrestricted Resource Consumption — No rate limiting
- Broken Function Level Authorization — Non-admin can call admin endpoints
- Server-Side Request Forgery (SSRF) — API fetches internal URLs
- Security Misconfiguration — Verbose errors, open CORS
- Lack of Input Validation — Injection, oversized payloads
- Improper Asset Management — Old API versions still running
- 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
| Check | Status |
|---|---|
| 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