mcpField
A
api-security-hardener

api-security-hardener

github.com/patricio0312rev安全63275 次安装41 周前更新
代码仓库

关于 api-security-hardener

帮助开发者为生产 API 规划和实现安全措施的技能,覆盖输入验证、身份验证、授权、限流及安全日志。还包含 Helmet 安全头、SQL 注入与 XSS 防护示例。

主要功能

  1. 01Zod 输入验证
  2. 02身份验证与角色授权
  3. 03API 限流
  4. 04Helmet 安全头配置
  5. 05SQL 注入与 XSS 防护
  6. 06请求日志与错误处理

适用场景

  1. 01为 Express API 添加验证中间件
  2. 02配置基于角色的访问控制
  3. 03为 API 请求设置限流与日志
  4. 04检查生产 API 的安全措施清单

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/api-security-hardener确认已加载。

详细介绍

API 安全加固

为生产环境中的 API 实施全面的安全措施。

核心工作流

  1. 输入验证:清理并验证所有输入
  2. 身份验证:安全地验证身份
  3. 授权:基于角色的访问控制
  4. 速率限制:防止滥用
  5. 安全标头:HTTP 标头防护
  6. 日志记录与监控:检测威胁

输入验证

Zod 架构验证
// 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();
});
验证中间件
// 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
);

速率限制

// 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);

身份验证中间件

// 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();
}

授权中间件

// 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);

安全标头

// 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 注入防护

// 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 防护

// 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> = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#039;',
  };
  return text.replace(/[&<>"']/g, (m) => map[m]);
}

// JSON response escaping (Express)
app.set('json escape', true);

请求日志记录

// 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();
}

错误处理

// 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,
  });
}

最佳实践

  1. 验证所有内容:绝不信任客户端输入
  2. 使用参数化查询:防止 SQL 注入
  3. 清理输出:防止 XSS
  4. 限制速率:防止滥用
  5. 记录所有内容:启用审计跟踪
  6. 使用 HTTPS:始终加密传输中的数据
  7. 尽量减少响应内容:避免泄露信息
  8. 更新依赖项:修补漏洞

输出检查清单

每项 API 安全实现都应包括:

  • 使用架构进行输入验证
  • 身份验证中间件
  • 授权(RBAC/ABAC)
  • 速率限制
  • 安全标头(Helmet)
  • CORS 配置
  • SQL 注入防护
  • XSS 防护
  • 请求日志记录
  • 错误处理(不泄露信息)

评论

支持 Markdown