API Security Hardener
Implement comprehensive security measures for production APIs.
Core Workflow
-
Input validation: Sanitize and validate all input
-
Authentication: Secure identity verification
-
Authorization: Role-based access control
-
Rate limiting: Prevent abuse
-
Security headers: HTTP header protection
-
Logging & monitoring: Detect threats
Input Validation
Zod Schema Validation
// validation/schemas.ts import { z } from 'zod';
// Common schemas export const emailSchema = z.string().email().toLowerCase().trim();
export const passwordSchema = z .string() .min(8, 'Password must be at least 8 characters') .max(128, 'Password too long') .regex(/[A-Z]/, 'Password must contain uppercase letter') .regex(/[a-z]/, 'Password must contain lowercase letter') .regex(/[0-9]/, 'Password must contain number') .regex(/[^A-Za-z0-9]/, 'Password must contain special character');
export const uuidSchema = z.string().uuid();
export const paginationSchema = z.object({ page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().min(1).max(100).default(20), sortBy: z.string().optional(), sortOrder: z.enum(['asc', 'desc']).default('desc'), });
// User schemas export const createUserSchema = z.object({ email: emailSchema, password: passwordSchema, name: z.string().min(2).max(100).trim(), });
export const updateUserSchema = createUserSchema.partial().omit({ password: true });
// Sanitize HTML content export const sanitizedStringSchema = z.string().transform((val) => { return val .replace(/[<>]/g, '') // Remove < and > .replace(/javascript:/gi, '') // Remove javascript: protocol .replace(/on\w+=/gi, '') // Remove event handlers .trim(); });
Validation Middleware
// middleware/validate.ts import { Request, Response, NextFunction } from 'express'; import { z, ZodSchema } from 'zod';
interface ValidationSchemas { body?: ZodSchema; query?: ZodSchema; params?: ZodSchema; }
export function validate(schemas: ValidationSchemas) { return async (req: Request, res: Response, next: NextFunction) => { try { if (schemas.body) { req.body = await schemas.body.parseAsync(req.body); } if (schemas.query) { req.query = await schemas.query.parseAsync(req.query); } if (schemas.params) { req.params = await schemas.params.parseAsync(req.params); } next(); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: 'Validation Error', details: error.errors.map((e) => ({ field: e.path.join('.'), message: e.message, })), }); } next(error); } }; }
// Usage router.post( '/users', validate({ body: createUserSchema }), createUserHandler );
Rate Limiting
// middleware/rate-limit.ts 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 export const apiLimiter = rateLimit({ store: new RedisStore({ sendCommand: (...args: string[]) => redis.call(...args), }), windowMs: 60 * 1000, // 1 minute max: 100, // 100 requests per minute message: { error: 'Too Many Requests', message: 'Please try again later', retryAfter: 60, }, standardHeaders: true, legacyHeaders: false, keyGenerator: (req) => { // Use user ID if authenticated, otherwise IP return req.user?.id || req.ip; }, skip: (req) => { // Skip rate limiting for health checks return req.path === '/health'; }, });
// Stricter limit for authentication endpoints
export const authLimiter = rateLimit({
store: new RedisStore({
sendCommand: (...args: string[]) => redis.call(...args),
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: {
error: 'Too Many Attempts',
message: 'Account temporarily locked. Try again in 15 minutes.',
},
keyGenerator: (req) => auth:${req.ip}:${req.body?.email},
});
// Cost-based rate limiting for expensive operations export const costLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 1000, // points per hour keyGenerator: (req) => req.user?.id || req.ip, handler: (req, res) => { res.status(429).json({ error: 'Rate Limit Exceeded', message: 'Hourly quota exceeded', }); }, });
// Usage with cost assignment router.post('/expensive-operation', (req, res, next) => { req.rateLimit = { ...req.rateLimit, current: req.rateLimit.current + 10 }; next(); }, costLimiter, handler);
Authentication Middleware
// middleware/auth.ts import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken';
interface JWTPayload { sub: string; email: string; role: string; iat: number; exp: number; }
declare global { namespace Express { interface Request { user?: JWTPayload; } } }
export function authenticate(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) { return res.status(401).json({ error: 'Unauthorized', message: 'Missing or invalid authorization header', }); }
const token = authHeader.slice(7);
try { const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
// Check token expiration with buffer
if (payload.exp * 1000 < Date.now()) {
return res.status(401).json({
error: 'Token Expired',
message: 'Please re-authenticate',
});
}
req.user = payload;
next();
} catch (error) { if (error instanceof jwt.TokenExpiredError) { return res.status(401).json({ error: 'Token Expired', message: 'Please re-authenticate', }); } if (error instanceof jwt.JsonWebTokenError) { return res.status(401).json({ error: 'Invalid Token', message: 'Token validation failed', }); } next(error); } }
// Optional authentication export function optionalAuth(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) { return next(); }
const token = authHeader.slice(7);
try { req.user = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload; } catch { // Ignore invalid tokens for optional auth }
next(); }
Authorization Middleware
// middleware/authorize.ts import { Request, Response, NextFunction } from 'express';
type Role = 'admin' | 'user' | 'guest';
interface Permission { resource: string; actions: string[]; }
const rolePermissions: Record<Role, Permission[]> = { admin: [ { resource: '', actions: [''] }, ], user: [ { resource: 'posts', actions: ['read', 'create', 'update:own', 'delete:own'] }, { resource: 'comments', actions: ['read', 'create', 'update:own', 'delete:own'] }, { resource: 'profile', actions: ['read', 'update'] }, ], guest: [ { resource: 'posts', actions: ['read'] }, { resource: 'comments', actions: ['read'] }, ], };
export function authorize(...roles: Role[]) { return (req: Request, res: Response, next: NextFunction) => { if (!req.user) { return res.status(401).json({ error: 'Unauthorized', message: 'Authentication required', }); }
const userRole = req.user.role as Role;
if (!roles.includes(userRole)) {
return res.status(403).json({
error: 'Forbidden',
message: 'Insufficient permissions',
});
}
next();
}; }
export function hasPermission(resource: string, action: string) { return (req: Request, res: Response, next: NextFunction) => { if (!req.user) { return res.status(401).json({ error: 'Unauthorized' }); }
const userRole = req.user.role as Role;
const permissions = rolePermissions[userRole];
const hasAccess = permissions.some((perm) => {
const resourceMatch = perm.resource === '*' || perm.resource === resource;
const actionMatch = perm.actions.includes('*') || perm.actions.includes(action);
return resourceMatch && actionMatch;
});
if (!hasAccess) {
return res.status(403).json({
error: 'Forbidden',
message: `Cannot ${action} ${resource}`,
});
}
next();
}; }
// Usage router.delete('/posts/:id', authenticate, hasPermission('posts', 'delete'), deletePost); router.get('/admin/users', authenticate, authorize('admin'), listUsers);
Security Headers
// middleware/security-headers.ts import helmet from 'helmet'; import { Express } from 'express';
export function configureSecurityHeaders(app: Express) { app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", 'data:', 'https:'], connectSrc: ["'self'", 'https://api.example.com'], fontSrc: ["'self'"], objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'none'"], }, }, crossOriginEmbedderPolicy: false, crossOriginResourcePolicy: { policy: 'cross-origin' }, }));
// Additional security headers app.use((req, res, next) => { // Prevent caching of sensitive data if (req.path.startsWith('/api/')) { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); }
// Permissions Policy
res.setHeader(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=(), interest-cohort=()'
);
next();
}); }
SQL Injection Prevention
// db/queries.ts - Using parameterized queries import { Pool } from 'pg';
const pool = new Pool();
// GOOD: Parameterized query export async function getUserById(id: string) { const result = await pool.query( 'SELECT * FROM users WHERE id = $1', [id] ); return result.rows[0]; }
// GOOD: Using query builder (Prisma) export async function searchUsers(term: string) { return prisma.user.findMany({ where: { OR: [ { name: { contains: term, mode: 'insensitive' } }, { email: { contains: term, mode: 'insensitive' } }, ], }, }); }
// BAD: String interpolation (vulnerable)
// const result = await pool.query(SELECT * FROM users WHERE id = '${id}');
XSS Prevention
// utils/sanitize.ts import DOMPurify from 'isomorphic-dompurify';
// Sanitize HTML content export function sanitizeHtml(dirty: string): string { return DOMPurify.sanitize(dirty, { ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'], ALLOWED_ATTR: ['href', 'target'], }); }
// Escape for plain text display export function escapeHtml(text: string): string { const map: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }; return text.replace(/[&<>"']/g, (m) => map[m]); }
// JSON response escaping (Express) app.set('json escape', true);
Request Logging
// middleware/logging.ts import { Request, Response, NextFunction } from 'express'; import { v4 as uuidv4 } from 'uuid';
export function requestLogger(req: Request, res: Response, next: NextFunction) { const requestId = req.headers['x-request-id'] as string || uuidv4(); const startTime = Date.now();
// Add request ID to response res.setHeader('X-Request-ID', requestId); req.requestId = requestId;
// Log request console.log(JSON.stringify({ type: 'request', requestId, method: req.method, path: req.path, query: req.query, ip: req.ip, userAgent: req.headers['user-agent'], userId: req.user?.sub, timestamp: new Date().toISOString(), }));
// Log response res.on('finish', () => { const duration = Date.now() - startTime;
console.log(JSON.stringify({
type: 'response',
requestId,
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration,
userId: req.user?.sub,
timestamp: new Date().toISOString(),
}));
// Alert on suspicious activity
if (res.statusCode === 401 || res.statusCode === 403) {
console.warn(JSON.stringify({
type: 'security_event',
event: 'access_denied',
requestId,
ip: req.ip,
path: req.path,
statusCode: res.statusCode,
}));
}
});
next(); }
Error Handling
// middleware/error-handler.ts import { Request, Response, NextFunction } from 'express';
export class AppError extends Error { constructor( public statusCode: number, public message: string, public code?: string ) { super(message); this.name = 'AppError'; } }
export function errorHandler( err: Error, req: Request, res: Response, next: NextFunction ) { console.error({ type: 'error', requestId: req.requestId, error: err.message, stack: process.env.NODE_ENV === 'development' ? err.stack : undefined, });
if (err instanceof AppError) { return res.status(err.statusCode).json({ error: err.name, message: err.message, code: err.code, }); }
// Don't leak internal errors to clients res.status(500).json({ error: 'Internal Server Error', message: 'An unexpected error occurred', requestId: req.requestId, }); }
Best Practices
-
Validate everything: Never trust client input
-
Use parameterized queries: Prevent SQL injection
-
Sanitize output: Prevent XSS
-
Rate limit: Protect against abuse
-
Log everything: Enable audit trails
-
Use HTTPS: Always encrypt in transit
-
Minimal responses: Don't leak information
-
Update dependencies: Patch vulnerabilities
Output Checklist
Every API security implementation should include:
-
Input validation with schemas
-
Authentication middleware
-
Authorization (RBAC/ABAC)
-
Rate limiting
-
Security headers (Helmet)
-
CORS configuration
-
SQL injection prevention
-
XSS prevention
-
Request logging
-
Error handling (no leaks)