关于 secure-error-handling
面向 API 的安全错误处理技能,通过区分开发和生产环境的响应,减少错误信息泄露。它提供多种 HTTP 错误处理器,并建议在服务器端记录详情、向生产客户端返回通用错误。
主要功能
- 01区分开发与生产环境错误响应
- 02提供 5 种 HTTP 错误处理器
- 03在服务器端记录错误详情
- 04避免日志记录敏感字段
- 05格式化验证错误信息
适用场景
- 01处理 API 中未预期的异常
- 02返回认证和授权错误
- 03报告输入验证问题
- 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 状态码
✅ 向客户端发送错误消息前先进行清理
参考资料
- OWASP 错误处理速查表:https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html
- OWASP Top 10 2021 - A04 不安全设计:https://owasp.org/Top10/A04_2021-Insecure_Design/
- HTTP 状态码:https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
- Node.js 错误处理:https://nodejs.org/api/errors.html
后续步骤
- 对于输入验证错误:使用
input-validationskill 和validateRequest() - 对于身份验证错误:使用
auth-securityskill - 对于测试错误响应:使用
security-testingskill - 对于完整的 API 安全:适当组合所有错误处理程序