secure-error-handling
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSecure Error Handling - Preventing Information Leakage
安全错误处理——防止信息泄露
The Error Message Problem
错误消息的问题
Error messages are designed to help developers debug. But in production, detailed errors help attackers more than they help users.
错误消息的设计初衷是帮助开发者调试。但在生产环境中,详细的错误信息对攻击者的帮助远大于对用户的帮助。
What Attackers Learn from Error Messages
攻击者能从错误消息中获取什么信息
Database structure:
Error: column 'credit_cards.number' does not exist→ Attacker now knows you have a table
credit_cardsFile paths:
Error at /var/www/app/lib/payment.js:47→ Attacker learns your directory structure
Dependencies:
Stripe API error: Invalid API key format→ Attacker knows you use Stripe
System info:
PostgreSQL 9.4 connection failed→ Attacker learns your database version and can look up known vulnerabilities
数据库结构:
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→ 攻击者了解你的数据库版本,进而可以查找已知漏洞
Real-World Information Leakage
真实场景中的信息泄露
According to SANS Institute research, 74% of successful attacks start with reconnaissance phase where attackers gather information about the target system. Error messages are a primary source of this intelligence.
Equifax Breach (2017):
Detailed error messages revealed they were using Apache Struts with a known vulnerability. Attackers exploited this revealed information.
根据SANS研究所的研究,74%的成功攻击始于侦察阶段,攻击者会在此阶段收集目标系统的信息。错误消息是这类情报的主要来源。
2017年Equifax数据泄露事件:
详细的错误消息暴露了他们使用的Apache Struts存在已知漏洞,攻击者利用这一泄露的信息发起了攻击。
Our Error Handling Architecture
我们的错误处理架构
Environment-Aware Error Responses
感知环境的错误响应
Development Mode:
javascript
{
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"
}→ Developers get full details for debugging
Production Mode:
javascript
{
error: "Internal server error",
message: "An unexpected error occurred. Please try again later."
}→ Users get safe, generic message
开发模式:
javascript
{
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"
}→ 开发者获取完整细节用于调试
生产模式:
javascript
{
error: "Internal server error",
message: "An unexpected error occurred. Please try again later."
}→ 用户收到安全的通用消息
The Logging Strategy
日志策略
All errors are logged server-side with full details (for investigation), but only generic messages are sent to clients in production. This gives us debugging capability without information leakage.
所有错误都会在服务器端记录完整细节(用于问题排查),但生产环境中仅向客户端返回通用消息。这样既能保留调试能力,又能避免信息泄露。
Implementation Files
实现文件
- - 5 error handlers for different scenarios
lib/errorHandler.ts
- - 针对不同场景的5个错误处理器
lib/errorHandler.ts
Available Error Handlers
可用的错误处理器
1. handleApiError(error, context)
1. handleApiError(error, context)
Use for: Unexpected errors (HTTP 500)
typescript
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
}
}Returns:
- Development: Full error with stack trace
- Production: Generic "Internal server error" message
- HTTP Status: 500
适用场景: 意外错误(HTTP 500)
typescript
import { handleApiError } from '@/lib/errorHandler';
async function handler(request: NextRequest) {
try {
// 高风险操作
await processPayment(data);
return NextResponse.json({ success: true });
} catch (error) {
return handleApiError(error, 'payment-processing');
// 生产环境:返回“Internal server error”
// 开发环境:返回完整堆栈跟踪
}
}返回结果:
- 开发环境: 包含堆栈跟踪的完整错误信息
- 生产环境: 通用的“Internal server error”消息
- HTTP状态码: 500
2. handleValidationError(message, details)
2. handleValidationError(message, details)
Use for: Input validation failures (HTTP 400)
typescript
import { handleValidationError } from '@/lib/errorHandler';
if (!isValidEmail(email)) {
return handleValidationError(
'Validation failed',
{ email: 'Invalid email format' }
);
}Returns:
json
{
"error": "Validation failed",
"details": {
"email": "Invalid email format"
}
}- HTTP Status: 400
- Both dev and production: Returns detailed field errors (helps users fix input)
适用场景: 输入验证失败(HTTP 400)
typescript
import { handleValidationError } from '@/lib/errorHandler';
if (!isValidEmail(email)) {
return handleValidationError(
'Validation failed',
{ email: 'Invalid email format' }
);
}返回结果:
json
{
"error": "Validation failed",
"details": {
"email": "Invalid email format"
}
}- HTTP状态码: 400
- 开发与生产环境一致: 返回详细的字段错误信息(帮助用户修正输入)
3. handleForbiddenError(message)
3. handleForbiddenError(message)
Use for: Authorization failures (HTTP 403)
typescript
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');
}Returns:
json
{
"error": "Forbidden",
"message": "You do not have access to this resource"
}- HTTP Status: 403
- Both dev and production: Returns the provided message
适用场景: 授权失败(HTTP 403)
typescript
import { handleForbiddenError } from '@/lib/errorHandler';
// 检查用户是否拥有该资源
if (resource.userId !== userId) {
return handleForbiddenError('You do not have access to this resource');
}返回结果:
json
{
"error": "Forbidden",
"message": "You do not have access to this resource"
}- HTTP状态码: 403
- 开发与生产环境一致: 返回传入的消息内容
4. handleUnauthorizedError(message)
4. handleUnauthorizedError(message)
Use for: Authentication failures (HTTP 401)
typescript
import { handleUnauthorizedError } from '@/lib/errorHandler';
import { auth } from '@clerk/nextjs/server';
const { userId } = await auth();
if (!userId) {
return handleUnauthorizedError('Authentication required');
}Returns:
json
{
"error": "Unauthorized",
"message": "Authentication required"
}- HTTP Status: 401
- Both dev and production: Returns the provided message
- Default message: "Authentication required" if no message provided
适用场景: 认证失败(HTTP 401)
typescript
import { handleUnauthorizedError } from '@/lib/errorHandler';
import { auth } from '@clerk/nextjs/server';
const { userId } = await auth();
if (!userId) {
return handleUnauthorizedError('Authentication required');
}返回结果:
json
{
"error": "Unauthorized",
"message": "Authentication required"
}- HTTP状态码: 401
- 开发与生产环境一致: 返回传入的消息内容
- 默认消息: 若未传入消息,则返回“Authentication required”
5. handleNotFoundError(resource)
5. handleNotFoundError(resource)
Use for: Resource not found (HTTP 404)
typescript
import { handleNotFoundError } from '@/lib/errorHandler';
const post = await db.posts.findOne({ id: postId });
if (!post) {
return handleNotFoundError('Post');
}Returns:
json
{
"error": "Not found",
"message": "Post not found"
}- HTTP Status: 404
- Both dev and production: Returns resource-specific message
适用场景: 资源不存在(HTTP 404)
typescript
import { handleNotFoundError } from '@/lib/errorHandler';
const post = await db.posts.findOne({ id: postId });
if (!post) {
return handleNotFoundError('Post');
}返回结果:
json
{
"error": "Not found",
"message": "Post not found"
}- HTTP状态码: 404
- 开发与生产环境一致: 返回与资源相关的消息
Complete Error Handling Examples
完整错误处理示例
Example 1: Protected API Route with Full Error Handling
示例1:带有完整错误处理的受保护API路由
typescript
// 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');
}
}typescript
// 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 {
// 认证检查
const { userId } = await auth();
if (!userId) {
return handleUnauthorizedError('Please sign in to view posts');
}
// 验证ID参数
const validation = validateRequest(idSchema, params.id);
if (!validation.success) {
return handleValidationError('Invalid post ID', { id: 'Must be valid ID' });
}
const postId = validation.data;
// 获取文章
const post = await db.posts.findOne({ id: postId });
// 处理资源不存在的情况
if (!post) {
return handleNotFoundError('Post');
}
// 检查授权
if (post.userId !== userId && !post.isPublic) {
return handleForbiddenError('You do not have access to this post');
}
return NextResponse.json({ post });
} catch (error) {
// 捕获意外错误
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');
}
// 仅文章作者可删除
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');
}
}Example 2: Payment Processing with Detailed Error Handling
示例2:带有详细错误处理的支付流程
typescript
// 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',
};typescript
// 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;
// 验证金额
if (!amount || amount < 50) {
return handleValidationError('Invalid amount', {
amount: 'Amount must be at least $0.50'
});
}
// 处理支付
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) {
// 处理Stripe特定错误
console.error('Stripe error:', stripeError);
// 不要向客户端暴露Stripe错误细节
if (stripeError.type === 'StripeCardError') {
return NextResponse.json(
{
error: 'Payment failed',
message: 'Your card was declined. Please try a different payment method.'
},
{ status: 400 }
);
}
// 其他Stripe问题返回通用错误
return NextResponse.json(
{
error: 'Payment processing failed',
message: 'Unable to process payment. Please try again later.'
},
{ status: 500 }
);
}
} catch (error) {
// 捕获所有意外错误
return handleApiError(error, 'process-payment');
}
}
export const POST = withRateLimit(withCsrf(paymentHandler));
export const config = {
runtime: 'nodejs',
};Example 3: Database Operation with Error Handling
示例3:带有错误处理的数据库操作
typescript
// 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');
}
}typescript
// 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();
}
// 用户仅能更新自己的资料
if (params.id !== userId) {
return handleForbiddenError('You can only update your own profile');
}
const body = await request.json();
// 验证输入
const validation = validateRequest(updateProfileSchema, body);
if (!validation.success) {
return validation.response;
}
const { displayName, bio, website } = validation.data;
// 更新资料
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) {
// 记录数据库错误用于调试
console.error('Database error:', dbError);
// 不要向客户端暴露数据库结构
if (dbError.code === 'UNIQUE_VIOLATION') {
return NextResponse.json(
{
error: 'Update failed',
message: 'This username is already taken'
},
{ status: 409 }
);
}
// 通用数据库错误
return NextResponse.json(
{
error: 'Database error',
message: 'Failed to update profile. Please try again.'
},
{ status: 500 }
);
}
} catch (error) {
return handleApiError(error, 'update-profile');
}
}Error Handler Implementation
错误处理器实现
lib/errorHandler.ts
lib/errorHandler.ts
typescript
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 }
);
}typescript
import { NextResponse } from 'next/server';
export function handleApiError(error: unknown, context: string) {
console.error(`[${context}] Error:`, error);
if (process.env.NODE_ENV === 'production') {
// 生产环境:返回通用错误
return NextResponse.json(
{
error: 'Internal server error',
message: 'An unexpected error occurred. Please try again later.'
},
{ status: 500 }
);
} else {
// 开发环境:返回完整错误细节
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 }
);
}Logging Best Practices
日志最佳实践
What to Log
应记录的内容
✅ Safe to Log:
- Error type/code
- Context (which operation failed)
- User ID (for tracking issues)
- Timestamp
- Request path
- HTTP status code
- IP addresses (for security monitoring)
- Operation names
- Last 4 digits of card (for reference only)
- Transaction IDs
❌ Never Log:
- Passwords (even hashed)
- Credit card numbers (full)
- CVV codes
- API keys/secrets/tokens
- Personal Identifiable Information (full addresses, SSN, etc.)
- Session tokens
- Encryption keys
- Full request/response bodies (may contain sensitive data)
- Environment variables ()
process.env - Full error stack traces (in production)
✅ 安全可记录的内容:
- 错误类型/代码
- 上下文(哪个操作失败)
- 用户ID(用于追踪问题)
- 时间戳
- 请求路径
- HTTP状态码
- IP地址(用于安全监控)
- 操作名称
- 银行卡后4位(仅用于参考)
- 交易ID
❌ 禁止记录的内容:
- 密码(即使是哈希后的)
- 完整银行卡号
- CVV码
- API密钥/机密/令牌
- 个人身份信息(完整地址、社保号等)
- Session令牌
- 加密密钥
- 完整请求/响应体(可能包含敏感数据)
- 环境变量()
process.env - 完整错误堆栈跟踪(生产环境中)
Secure Logging Example
安全日志示例
typescript
// ✅ 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
});typescript
// ✅ 良好的日志记录
console.error('Payment failed', {
userId,
errorCode: error.code,
errorType: error.type,
timestamp: new Date().toISOString(),
path: request.nextUrl.pathname
});
// ❌ 不良的日志记录
console.error('Payment failed', {
userId,
creditCard: cardNumber, // ❌ 禁止记录支付信息
apiKey: stripeKey, // ❌ 禁止记录机密信息
request: req.body // ❌ 可能包含敏感数据
});Redacting Sensitive Fields
敏感字段脱敏
Always redact sensitive data before logging:
typescript
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: 'user@example.com',
password: 'secret123' // Will be [REDACTED]
});记录前务必对敏感数据进行脱敏处理:
typescript
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);
}
// 使用示例
safelog({
userId: 'user123',
email: 'user@example.com',
password: 'secret123' // 会被替换为[REDACTED]
});Production Logging Setup
生产环境日志设置
typescript
// 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
}
});typescript
// 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') {
// 发送到日志服务(Vercel日志、Datadog等)
console.log(JSON.stringify(logEntry));
} else {
// 开发环境中格式化打印
console.log('Security Event:', logEntry);
}
}
// 使用示例
logSecurityEvent({
type: 'UNAUTHORIZED_ACCESS_ATTEMPT',
userId,
ip: request.ip,
details: {
path: request.nextUrl.pathname,
method: request.method
}
});Client-Side Error Handling
客户端错误处理
Graceful Error Display
优雅的错误展示
typescript
// 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>
);
}typescript
// 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 with Error Handling
带有错误处理的Fetch请求
typescript
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;
}
}typescript
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();
// 处理不同类型的错误
switch (response.status) {
case 400:
// 验证错误——展示字段错误
if (error.details) {
showFieldErrors(error.details);
}
break;
case 401:
// 重定向到登录页
router.push('/sign-in');
break;
case 403:
// 展示访问被拒绝消息
alert(error.message);
break;
case 429:
// 请求频率超限——展示重试提示
alert(`Too many requests. Please wait ${error.retryAfter} seconds.`);
break;
default:
// 通用错误
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;
}
}What Secure Error Handling Prevents
安全错误处理能防范的风险
✅ Information disclosure - No system details exposed
✅ System fingerprinting - Can't identify technology stack
✅ Database structure revelation - No schema details in errors
✅ Technology stack identification - Generic errors only
✅ Attack surface reconnaissance - Minimal information leakage
✅ Path disclosure - No file system paths exposed
✅ Version disclosure - No software versions revealed
✅ 信息泄露 - 不暴露系统细节
✅ 系统指纹识别 - 无法识别技术栈
✅ 数据库结构泄露 - 错误中不包含 schema 细节
✅ 技术栈识别 - 仅返回通用错误
✅ 攻击面侦察 - 最小化信息泄露
✅ 路径泄露 - 不暴露文件系统路径
✅ 版本泄露 - 不泄露软件版本
Common Mistakes to Avoid
需避免的常见错误
❌ DON'T return error.message directly to clients
❌ DON'T include stack traces in production responses
❌ DON'T expose database errors to clients
❌ DON'T log sensitive data (passwords, tokens, cards)
❌ DON'T use same error messages for dev and prod
❌ DON'T forget to log errors server-side for debugging
✅ DO use handleApiError() for unexpected errors
✅ DO use specific handlers for known error types
✅ DO log errors server-side with context
✅ DO return helpful (but safe) messages to users
✅ DO use appropriate HTTP status codes
✅ DO sanitize error messages before sending to client
❌ 不要直接将error.message返回给客户端
❌ 不要在生产环境响应中包含堆栈跟踪
❌ 不要向客户端暴露数据库错误
❌ 不要记录敏感数据(密码、令牌、银行卡信息)
❌ 开发与生产环境不要使用相同的错误消息
❌ 不要忘记在服务器端记录错误用于调试
✅ 使用handleApiError()处理意外错误
✅ 针对已知错误类型使用特定处理器
✅ 在服务器端记录带有上下文的错误
✅ 向用户返回有用但安全的消息
✅ 使用合适的HTTP状态码
✅ 向客户端发送前先清洗错误消息
References
参考资料
- OWASP Error Handling Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html
- OWASP Top 10 2021 - A04 Insecure Design: https://owasp.org/Top10/A04_2021-Insecure_Design/
- HTTP Status Codes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
- Node.js Error Handling: https://nodejs.org/api/errors.html
- OWASP错误处理 cheat sheet: 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
Next Steps
后续步骤
- For input validation errors: Use skill with
input-validationvalidateRequest() - For authentication errors: Use skill
auth-security - For testing error responses: Use skill
security-testing - For complete API security: Combine all error handlers appropriately
- 输入验证错误:结合技能与
input-validation使用validateRequest() - 认证错误:使用技能
auth-security - 测试错误响应:使用技能
security-testing - 完整API安全:合理组合所有错误处理器