mcpField
S
secure-error-handling

secure-error-handling

github.com/harperaa安全19196 次安装45 周前更新
代码仓库

关于 secure-error-handling

面向 API 的安全错误处理技能,通过区分开发和生产环境的响应,减少错误信息泄露。它提供多种 HTTP 错误处理器,并建议在服务器端记录详情、向生产客户端返回通用错误。

主要功能

  1. 01区分开发与生产环境错误响应
  2. 02提供 5 种 HTTP 错误处理器
  3. 03在服务器端记录错误详情
  4. 04避免日志记录敏感字段
  5. 05格式化验证错误信息

适用场景

  1. 01处理 API 中未预期的异常
  2. 02返回认证和授权错误
  3. 03报告输入验证问题
  4. 04避免生产响应泄露内部细节

安装

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

$npx skills add https://github.com/harperaa/secure-claude-skills --skill secure-error-handling

安装后在 Claude Code 中输入/secure-error-handling确认已加载。

详细介绍

安全错误处理 - 防止信息泄露

错误消息的问题

错误消息旨在帮助开发者调试。但在生产环境中,详细的错误对攻击者的帮助往往大于对用户的帮助。

攻击者能从错误消息中了解到什么

数据库结构:

Error: column 'credit_cards.number' does not exist

→ 攻击者现在知道你有一个 credit_cards 表

文件路径:

Error at /var/www/app/lib/payment.js:47

→ 攻击者会了解到你的目录结构

依赖项:

Stripe API error: Invalid API key format

→ 攻击者知道你使用 Stripe

系统信息:

PostgreSQL 9.4 connection failed

→ 攻击者会了解到你的数据库版本,并能查找已知漏洞

现实中的信息泄露

根据 SANS Institute 的研究,74% 的成功攻击始于侦察阶段,攻击者会在此阶段收集目标系统的信息。错误消息是这类情报的主要来源。

Equifax 数据泄露事件(2017 年): 详细的错误消息暴露了他们使用了存在已知漏洞的 Apache Struts。攻击者利用了这一泄露的信息。

我们的错误处理架构

根据环境返回错误响应

开发模式:

{
  error: "Database connection failed",
  stack: "Error: connection timeout at db.connect (database.js:42:15)...",
  context: "user-profile-update",
  timestamp: "2025-10-15T10:30:00Z"
}

→ 开发者可以获取完整详情以进行调试

生产模式:

{
  error: "Internal server error",
  message: "An unexpected error occurred. Please try again later."
}

→ 用户会收到安全、通用的消息

日志记录策略

所有错误都会在服务器端记录完整详情(用于调查),但在生产环境中只向客户端发送通用消息。这样既能进行调试,又不会泄露信息。

实现文件

  • lib/errorHandler.ts - 用于不同场景的 5 个错误处理器

可用的错误处理器

1. handleApiError(error, context)

用于: 意外错误(HTTP 500)

import { handleApiError } from '@/lib/errorHandler';

async function handler(request: NextRequest) {
  try {
    // Risky operation
    await processPayment(data);
    return NextResponse.json({ success: true });

  } catch (error) {
    return handleApiError(error, 'payment-processing');
    // Production: "Internal server error"
    // Development: Full stack trace
  }
}

返回:

  • 开发环境: 包含堆栈跟踪的完整错误
  • 生产环境: 通用的“内部服务器错误”消息
  • HTTP 状态码: 500
2. handleValidationError(message, details)

用于: 输入验证失败(HTTP 400)

import { handleValidationError } from '@/lib/errorHandler';

if (!isValidEmail(email)) {
  return handleValidationError(
    'Validation failed',
    { email: 'Invalid email format' }
  );
}

返回:

{
  "error": "Validation failed",
  "details": {
    "email": "Invalid email format"
  }
}
  • HTTP 状态码: 400
  • 开发和生产环境: 均返回详细的字段错误(帮助用户修正输入)
3. handleForbiddenError(message)

用于: 授权失败(HTTP 403)

import { handleForbiddenError } from '@/lib/errorHandler';

// Check if user owns this resource
if (resource.userId !== userId) {
  return handleForbiddenError('You do not have access to this resource');
}

返回:

{
  "error": "Forbidden",
  "message": "You do not have access to this resource"
}
  • HTTP 状态码: 403
  • 开发和生产环境: 均返回提供的消息
4. handleUnauthorizedError(message)

用于: 身份验证失败(HTTP 401)

import { handleUnauthorizedError } from '@/lib/errorHandler';
import { auth } from '@clerk/nextjs/server';

const { userId } = await auth();
if (!userId) {
  return handleUnauthorizedError('Authentication required');
}

返回:

{
  "error": "Unauthorized",
  "message": "Authentication required"
}
  • HTTP 状态码: 401
  • 开发和生产环境: 均返回提供的消息
  • 默认消息: 如果未提供消息,则为“需要身份验证”
5. handleNotFoundError(resource)

用于: 未找到资源(HTTP 404)

import { handleNotFoundError } from '@/lib/errorHandler';

const post = await db.posts.findOne({ id: postId });
if (!post) {
  return handleNotFoundError('Post');
}

返回:

{
  "error": "Not found",
  "message": "Post not found"
}
  • HTTP 状态码: 404
  • 开发和生产环境: 均返回针对具体资源的消息

完整错误处理示例

示例 1:包含完整错误处理的受保护 API 路由
// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs/server';
import { validateRequest } from '@/lib/validateRequest';
import { idSchema } from '@/lib/validation';
import {
  handleApiError,
  handleUnauthorizedError,
  handleForbiddenError,
  handleNotFoundError,
  handleValidationError
} from '@/lib/errorHandler';

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    // Authentication check
    const { userId } = await auth();
    if (!userId) {
      return handleUnauthorizedError('Please sign in to view posts');
    }

    // Validate ID parameter
    const validation = validateRequest(idSchema, params.id);
    if (!validation.success) {
      return handleValidationError('Invalid post ID', { id: 'Must be valid ID' });
    }

    const postId = validation.data;

    // Fetch post
    const post = await db.posts.findOne({ id: postId });

    // Handle not found
    if (!post) {
      return handleNotFoundError('Post');
    }

    // Check authorization
    if (post.userId !== userId && !post.isPublic) {
      return handleForbiddenError('You do not have access to this post');
    }

    return NextResponse.json({ post });

  } catch (error) {
    // Catch unexpected errors
    return handleApiError(error, 'get-post');
  }
}

export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const { userId } = await auth();
    if (!userId) {
      return handleUnauthorizedError();
    }

    const validation = validateRequest(idSchema, params.id);
    if (!validation.success) {
      return handleValidationError('Invalid post ID', validation.error);
    }

    const postId = validation.data;
    const post = await db.posts.findOne({ id: postId });

    if (!post) {
      return handleNotFoundError('Post');
    }

    // Only post owner can delete
    if (post.userId !== userId) {
      return handleForbiddenError('Only the post author can delete this post');
    }

    await db.posts.delete({ id: postId });

    return NextResponse.json({ success: true });

  } catch (error) {
    return handleApiError(error, 'delete-post');
  }
}
示例 2:包含详细错误处理的支付处理
// app/api/process-payment/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { withRateLimit } from '@/lib/withRateLimit';
import { withCsrf } from '@/lib/withCsrf';
import { auth } from '@clerk/nextjs/server';
import { handleApiError, handleUnauthorizedError, handleValidationError } from '@/lib/errorHandler';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

async function paymentHandler(request: NextRequest) {
  try {
    const { userId } = await auth();
    if (!userId) {
      return handleUnauthorizedError();
    }

    const body = await request.json();
    const { amount, paymentMethodId } = body;

    // Validate amount
    if (!amount || amount < 50) {
      return handleValidationError('Invalid amount', {
        amount: 'Amount must be at least $0.50'
      });
    }

    // Process payment
    try {
      const paymentIntent = await stripe.paymentIntents.create({
        amount,
        currency: 'usd',
        payment_method: paymentMethodId,
        confirm: true,
        metadata: { userId }
      });

      return NextResponse.json({
        success: true,
        paymentIntentId: paymentIntent.id
      });

    } catch (stripeError: any) {
      // Handle Stripe-specific errors
      console.error('Stripe error:', stripeError);

      // Don't expose Stripe error details to client
      if (stripeError.type === 'StripeCardError') {
        return NextResponse.json(
          {
            error: 'Payment failed',
            message: 'Your card was declined. Please try a different payment method.'
          },
          { status: 400 }
        );
      }

      // Generic error for other Stripe issues
      return NextResponse.json(
        {
          error: 'Payment processing failed',
          message: 'Unable to process payment. Please try again later.'
        },
        { status: 500 }
      );
    }

  } catch (error) {
    // Catch-all for unexpected errors
    return handleApiError(error, 'process-payment');
  }
}

export const POST = withRateLimit(withCsrf(paymentHandler));

export const config = {
  runtime: 'nodejs',
};
示例 3:包含错误处理的数据库操作
// app/api/users/[id]/profile/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs/server';
import { validateRequest } from '@/lib/validateRequest';
import { updateProfileSchema } from '@/lib/validation';
import {
  handleApiError,
  handleUnauthorizedError,
  handleForbiddenError,
  handleNotFoundError
} from '@/lib/errorHandler';

export async function PATCH(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const { userId } = await auth();
    if (!userId) {
      return handleUnauthorizedError();
    }

    // Users can only update their own profile
    if (params.id !== userId) {
      return handleForbiddenError('You can only update your own profile');
    }

    const body = await request.json();

    // Validate input
    const validation = validateRequest(updateProfileSchema, body);
    if (!validation.success) {
      return validation.response;
    }

    const { displayName, bio, website } = validation.data;

    // Update profile
    try {
      const updatedProfile = await db.profiles.update(
        { userId },
        {
          displayName,
          bio,
          website,
          updatedAt: Date.now()
        }
      );

      if (!updatedProfile) {
        return handleNotFoundError('Profile');
      }

      return NextResponse.json({ profile: updatedProfile });

    } catch (dbError: any) {
      // Log database error for debugging
      console.error('Database error:', dbError);

      // Don't expose database structure to client
      if (dbError.code === 'UNIQUE_VIOLATION') {
        return NextResponse.json(
          {
            error: 'Update failed',
            message: 'This username is already taken'
          },
          { status: 409 }
        );
      }

      // Generic database error
      return NextResponse.json(
        {
          error: 'Database error',
          message: 'Failed to update profile. Please try again.'
        },
        { status: 500 }
      );
    }

  } catch (error) {
    return handleApiError(error, 'update-profile');
  }
}

错误处理器实现

lib/errorHandler.ts
import { NextResponse } from 'next/server';

export function handleApiError(error: unknown, context: string) {
  console.error(`[${context}] Error:`, error);

  if (process.env.NODE_ENV === 'production') {
    // Production: Generic error
    return NextResponse.json(
      {
        error: 'Internal server error',
        message: 'An unexpected error occurred. Please try again later.'
      },
      { status: 500 }
    );
  } else {
    // Development: Full error details
    return NextResponse.json(
      {
        error: 'Internal server error',
        message: error instanceof Error ? error.message : 'Unknown error',
        stack: error instanceof Error ? error.stack : undefined,
        context,
        timestamp: new Date().toISOString()
      },
      { status: 500 }
    );
  }
}

export function handleValidationError(
  message: string,
  details: Record<string, string>
) {
  return NextResponse.json(
    {
      error: 'Validation failed',
      message,
      details
    },
    { status: 400 }
  );
}

export function handleForbiddenError(message?: string) {
  return NextResponse.json(
    {
      error: 'Forbidden',
      message: message || 'Access denied'
    },
    { status: 403 }
  );
}

export function handleUnauthorizedError(message?: string) {
  return NextResponse.json(
    {
      error: 'Unauthorized',
      message: message || 'Authentication required'
    },
    { status: 401 }
  );
}

export function handleNotFoundError(resource: string) {
  return NextResponse.json(
    {
      error: 'Not found',
      message: `${resource} not found`
    },
    { status: 404 }
  );
}

日志记录最佳实践

记录哪些内容

✅ 可以安全记录:

  • 错误类型/代码
  • 上下文(失败的操作)
  • 用户 ID(用于跟踪问题)
  • 时间戳
  • 请求路径
  • HTTP 状态码
  • IP 地址(用于安全监控)
  • 操作名称
  • 卡号后 4 位(仅供参考)
  • 交易 ID

❌ 绝不记录:

  • 密码(即使经过哈希处理)
  • 完整的信用卡号
  • CVV 码
  • API 密钥/密钥/令牌
  • 个人身份信息(完整地址、SSN 等)
  • 会话令牌
  • 加密密钥
  • 完整的请求/响应正文(可能包含敏感数据)
  • 环境变量(process.env)
  • 完整的错误堆栈跟踪(生产环境中)
安全日志记录示例
// ✅ Good logging
console.error('Payment failed', {
  userId,
  errorCode: error.code,
  errorType: error.type,
  timestamp: new Date().toISOString(),
  path: request.nextUrl.pathname
});

// ❌ Bad logging
console.error('Payment failed', {
  userId,
  creditCard: cardNumber, // ❌ Never log payment info
  apiKey: stripeKey,      // ❌ Never log secrets
  request: req.body       // ❌ May contain sensitive data
});
隐去敏感字段

记录日志前,务必隐去敏感数据:

const SENSITIVE_FIELDS = [
  'password', 'token', 'secret', 'apiKey', 'ssn',
  'creditCard', 'cvv', 'cardNumber'
];

function safelog(data: any) {
  const sanitized = { ...data };
  SENSITIVE_FIELDS.forEach(field => {
    if (field in sanitized) {
      sanitized[field] = '[REDACTED]';
    }
  });
  console.log(sanitized);
}

// Usage
safelog({
  userId: 'user123',
  email: '[email protected]',
  password: 'secret123' // Will be [REDACTED]
});
生产环境日志设置
// lib/logger.ts
export function logSecurityEvent(event: {
  type: string;
  userId?: string;
  ip?: string;
  details?: Record<string, any>;
}) {
  const logEntry = {
    ...event,
    timestamp: new Date().toISOString(),
    environment: process.env.NODE_ENV
  };

  if (process.env.NODE_ENV === 'production') {
    // Send to logging service (Vercel logs, Datadog, etc.)
    console.log(JSON.stringify(logEntry));
  } else {
    // Pretty print in development
    console.log('Security Event:', logEntry);
  }
}

// Usage
logSecurityEvent({
  type: 'UNAUTHORIZED_ACCESS_ATTEMPT',
  userId,
  ip: request.ip,
  details: {
    path: request.nextUrl.pathname,
    method: request.method
  }
});

客户端错误处理

优雅地显示错误
// components/ErrorDisplay.tsx
export function ErrorDisplay({ error }: { error: ApiError }) {
  const getMessage = () => {
    switch (error.status) {
      case 400:
        return error.details
          ? Object.entries(error.details).map(([field, msg]) =>
              `${field}: ${msg}`
            ).join(', ')
          : 'Invalid input. Please check your data.';
      case 401:
        return 'Please sign in to continue.';
      case 403:
        return 'You don\'t have permission to do that.';
      case 404:
        return 'The requested resource was not found.';
      case 429:
        return 'Too many requests. Please wait a moment.';
      case 500:
        return 'Something went wrong. Please try again later.';
      default:
        return 'An error occurred. Please try again.';
    }
  };

  return (
    <div className="error-message">
      {getMessage()}
    </div>
  );
}
带错误处理的 Fetch
async function createPost(data: PostData) {
  try {
    const response = await fetch('/api/posts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });

    if (!response.ok) {
      const error = await response.json();

      // Handle different error types
      switch (response.status) {
        case 400:
          // Validation error - show field errors
          if (error.details) {
            showFieldErrors(error.details);
          }
          break;
        case 401:
          // Redirect to login
          router.push('/sign-in');
          break;
        case 403:
          // Show access denied message
          alert(error.message);
          break;
        case 429:
          // Rate limited - show retry message
          alert(`Too many requests. Please wait ${error.retryAfter} seconds.`);
          break;
        default:
          // Generic error
          alert('An error occurred. Please try again.');
      }

      return null;
    }

    return await response.json();

  } catch (error) {
    console.error('Network error:', error);
    alert('Network error. Please check your connection.');
    return null;
  }
}

安全错误处理可防范的问题

✅ 信息泄露 - 不会暴露系统详情
✅ 系统指纹识别 - 无法识别技术栈
✅ 数据库结构泄露 - 错误中不包含架构详情
✅ 技术栈识别 - 仅返回通用错误
✅ 攻击面侦察 - 尽可能减少信息泄露
✅ 路径泄露 - 不会暴露文件系统路径
✅ 版本泄露 - 不会透露软件版本

应避免的常见错误

❌ 不要直接将 error.message 返回给客户端
❌ 不要在生产环境响应中包含堆栈跟踪
❌ 不要向客户端暴露数据库错误
❌ 不要记录敏感数据(密码、令牌、银行卡)
❌ 不要在开发环境和生产环境使用相同的错误消息
❌ 不要忘记在服务端记录错误以便调试

✅ 对意外错误使用 handleApiError()
✅ 对已知错误类型使用专用处理程序
✅ 在服务端记录错误及上下文信息
✅ 向用户返回有帮助(但安全)的消息
✅ 使用适当的 HTTP 状态码
✅ 向客户端发送错误消息前先进行清理

参考资料

后续步骤

  • 对于输入验证错误:使用 input-validation skill 和 validateRequest()
  • 对于身份验证错误:使用 auth-security skill
  • 对于测试错误响应:使用 security-testing skill
  • 对于完整的 API 安全:适当组合所有错误处理程序

评论

支持 Markdown