api-security-hardener

安装量: 42
排名: #17218

安装

npx skills add https://github.com/patricio0312rev/skills --skill api-security-hardener

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 = { 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 = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }; 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)

返回排行榜