webhook-security
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWebhook Security
Webhook 安全
Production-ready webhook handling with defense in depth.
可用于生产环境的深度防御式Webhook处理方案。
When to Use This Skill
适用场景
- Receiving webhooks from payment providers (Stripe, PayPal)
- Integrating with GitHub, GitLab, or other dev tools
- Building your own webhook delivery system
- Any endpoint receiving external POST requests
- 接收来自支付服务商(Stripe、PayPal)的Webhook
- 与GitHub、GitLab或其他开发工具集成
- 构建自有Webhook分发系统
- 任何接收外部POST请求的端点
Security Layers
安全防护层级
┌─────────────────────────────────────────────────────┐
│ Incoming Webhook │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 1. Signature Verification (HMAC-SHA256) │
│ - Reject if signature invalid │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 2. Timestamp Validation │
│ - Reject if older than 5 minutes │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 3. Idempotency Check │
│ - Skip if already processed │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 4. Process Webhook │
│ - Handle business logic │
└─────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────┐
│ Incoming Webhook │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 1. 签名验证(HMAC-SHA256) │
│ - 签名无效则拒绝请求 │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 2. 时间戳验证 │
│ - 请求超过5分钟则拒绝 │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 3. 幂等性检查 │
│ - 已处理过的请求则跳过 │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 4. 处理Webhook请求 │
│ - 执行业务逻辑 │
└─────────────────────────────────────────────────────┘TypeScript Implementation
TypeScript 实现
Signature Verification
签名验证
typescript
// webhook-verifier.ts
import crypto from 'crypto';
interface WebhookConfig {
secret: string;
signatureHeader: string;
timestampHeader?: string;
tolerance?: number; // seconds
}
interface VerificationResult {
valid: boolean;
error?: string;
}
class WebhookVerifier {
constructor(private config: WebhookConfig) {}
verify(payload: string | Buffer, headers: Record<string, string>): VerificationResult {
const signature = headers[this.config.signatureHeader.toLowerCase()];
if (!signature) {
return { valid: false, error: 'Missing signature header' };
}
// Check timestamp if configured
if (this.config.timestampHeader) {
const timestamp = headers[this.config.timestampHeader.toLowerCase()];
if (!timestamp) {
return { valid: false, error: 'Missing timestamp header' };
}
const timestampAge = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
const tolerance = this.config.tolerance || 300; // 5 minutes default
if (Math.abs(timestampAge) > tolerance) {
return { valid: false, error: 'Timestamp outside tolerance window' };
}
}
// Compute expected signature
const expectedSignature = this.computeSignature(payload, headers);
// Constant-time comparison to prevent timing attacks
const valid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
return { valid, error: valid ? undefined : 'Invalid signature' };
}
private computeSignature(payload: string | Buffer, headers: Record<string, string>): string {
const timestamp = this.config.timestampHeader
? headers[this.config.timestampHeader.toLowerCase()]
: '';
const signedPayload = timestamp ? `${timestamp}.${payload}` : payload.toString();
return 'sha256=' + crypto
.createHmac('sha256', this.config.secret)
.update(signedPayload)
.digest('hex');
}
}
export { WebhookVerifier, WebhookConfig, VerificationResult };typescript
// webhook-verifier.ts
import crypto from 'crypto';
interface WebhookConfig {
secret: string;
signatureHeader: string;
timestampHeader?: string;
tolerance?: number; // seconds
}
interface VerificationResult {
valid: boolean;
error?: string;
}
class WebhookVerifier {
constructor(private config: WebhookConfig) {}
verify(payload: string | Buffer, headers: Record<string, string>): VerificationResult {
const signature = headers[this.config.signatureHeader.toLowerCase()];
if (!signature) {
return { valid: false, error: 'Missing signature header' };
}
// Check timestamp if configured
if (this.config.timestampHeader) {
const timestamp = headers[this.config.timestampHeader.toLowerCase()];
if (!timestamp) {
return { valid: false, error: 'Missing timestamp header' };
}
const timestampAge = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
const tolerance = this.config.tolerance || 300; // 5 minutes default
if (Math.abs(timestampAge) > tolerance) {
return { valid: false, error: 'Timestamp outside tolerance window' };
}
}
// Compute expected signature
const expectedSignature = this.computeSignature(payload, headers);
// Constant-time comparison to prevent timing attacks
const valid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
return { valid, error: valid ? undefined : 'Invalid signature' };
}
private computeSignature(payload: string | Buffer, headers: Record<string, string>): string {
const timestamp = this.config.timestampHeader
? headers[this.config.timestampHeader.toLowerCase()]
: '';
const signedPayload = timestamp ? `${timestamp}.${payload}` : payload.toString();
return 'sha256=' + crypto
.createHmac('sha256', this.config.secret)
.update(signedPayload)
.digest('hex');
}
}
export { WebhookVerifier, WebhookConfig, VerificationResult };Provider-Specific Verifiers
服务商专属验证器
typescript
// providers/stripe.ts
import Stripe from 'stripe';
export function verifyStripeWebhook(
payload: string | Buffer,
signature: string,
secret: string
): Stripe.Event {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
// Stripe's library handles verification
return stripe.webhooks.constructEvent(payload, signature, secret);
}
// providers/github.ts
import crypto from 'crypto';
export function verifyGitHubWebhook(
payload: string,
signature: string,
secret: string
): boolean {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// providers/twilio.ts
import crypto from 'crypto';
export function verifyTwilioWebhook(
url: string,
params: Record<string, string>,
signature: string,
authToken: string
): boolean {
// Twilio uses URL + sorted params
const data = url + Object.keys(params)
.sort()
.map(key => key + params[key])
.join('');
const expected = crypto
.createHmac('sha1', authToken)
.update(data)
.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}typescript
// providers/stripe.ts
import Stripe from 'stripe';
export function verifyStripeWebhook(
payload: string | Buffer,
signature: string,
secret: string
): Stripe.Event {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
// Stripe's library handles verification
return stripe.webhooks.constructEvent(payload, signature, secret);
}
// providers/github.ts
import crypto from 'crypto';
export function verifyGitHubWebhook(
payload: string,
signature: string,
secret: string
): boolean {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// providers/twilio.ts
import crypto from 'crypto';
export function verifyTwilioWebhook(
url: string,
params: Record<string, string>,
signature: string,
authToken: string
): boolean {
// Twilio uses URL + sorted params
const data = url + Object.keys(params)
.sort()
.map(key => key + params[key])
.join('');
const expected = crypto
.createHmac('sha1', authToken)
.update(data)
.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}Idempotency Handler
幂等性处理器
typescript
// idempotency.ts
import { Redis } from 'ioredis';
interface IdempotencyConfig {
redis: Redis;
keyPrefix?: string;
ttlSeconds?: number;
}
class IdempotencyHandler {
private redis: Redis;
private keyPrefix: string;
private ttl: number;
constructor(config: IdempotencyConfig) {
this.redis = config.redis;
this.keyPrefix = config.keyPrefix || 'webhook:processed:';
this.ttl = config.ttlSeconds || 86400; // 24 hours
}
async isProcessed(eventId: string): Promise<boolean> {
const key = this.keyPrefix + eventId;
const exists = await this.redis.exists(key);
return exists === 1;
}
async markProcessed(eventId: string, result?: unknown): Promise<void> {
const key = this.keyPrefix + eventId;
const value = JSON.stringify({
processedAt: new Date().toISOString(),
result,
});
await this.redis.setex(key, this.ttl, value);
}
async getProcessedResult(eventId: string): Promise<unknown | null> {
const key = this.keyPrefix + eventId;
const value = await this.redis.get(key);
if (!value) return null;
return JSON.parse(value);
}
}
export { IdempotencyHandler, IdempotencyConfig };typescript
// idempotency.ts
import { Redis } from 'ioredis';
interface IdempotencyConfig {
redis: Redis;
keyPrefix?: string;
ttlSeconds?: number;
}
class IdempotencyHandler {
private redis: Redis;
private keyPrefix: string;
private ttl: number;
constructor(config: IdempotencyConfig) {
this.redis = config.redis;
this.keyPrefix = config.keyPrefix || 'webhook:processed:';
this.ttl = config.ttlSeconds || 86400; // 24 hours
}
async isProcessed(eventId: string): Promise<boolean> {
const key = this.keyPrefix + eventId;
const exists = await this.redis.exists(key);
return exists === 1;
}
async markProcessed(eventId: string, result?: unknown): Promise<void> {
const key = this.keyPrefix + eventId;
const value = JSON.stringify({
processedAt: new Date().toISOString(),
result,
});
await this.redis.setex(key, this.ttl, value);
}
async getProcessedResult(eventId: string): Promise<unknown | null> {
const key = this.keyPrefix + eventId;
const value = await this.redis.get(key);
if (!value) return null;
return JSON.parse(value);
}
}
export { IdempotencyHandler, IdempotencyConfig };Complete Webhook Handler
完整Webhook处理器
typescript
// webhook-handler.ts
import { Request, Response, NextFunction } from 'express';
import { WebhookVerifier } from './webhook-verifier';
import { IdempotencyHandler } from './idempotency';
interface WebhookHandlerConfig {
verifier: WebhookVerifier;
idempotency: IdempotencyHandler;
eventIdExtractor: (payload: unknown) => string;
}
function createWebhookHandler(config: WebhookHandlerConfig) {
return async (req: Request, res: Response, next: NextFunction) => {
// Get raw body (must use raw body parser)
const rawBody = req.body;
// 1. Verify signature
const verification = config.verifier.verify(
rawBody,
req.headers as Record<string, string>
);
if (!verification.valid) {
console.error('Webhook verification failed:', verification.error);
return res.status(401).json({ error: verification.error });
}
// Parse payload
const payload = JSON.parse(rawBody.toString());
// 2. Check idempotency
const eventId = config.eventIdExtractor(payload);
if (await config.idempotency.isProcessed(eventId)) {
console.log(`Webhook ${eventId} already processed, skipping`);
return res.status(200).json({ status: 'already_processed' });
}
// 3. Attach parsed payload and continue
req.body = payload;
(req as any).webhookEventId = eventId;
// 4. After processing, mark as processed
res.on('finish', async () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
await config.idempotency.markProcessed(eventId);
}
});
next();
};
}
export { createWebhookHandler };typescript
// webhook-handler.ts
import { Request, Response, NextFunction } from 'express';
import { WebhookVerifier } from './webhook-verifier';
import { IdempotencyHandler } from './idempotency';
interface WebhookHandlerConfig {
verifier: WebhookVerifier;
idempotency: IdempotencyHandler;
eventIdExtractor: (payload: unknown) => string;
}
function createWebhookHandler(config: WebhookHandlerConfig) {
return async (req: Request, res: Response, next: NextFunction) => {
// Get raw body (must use raw body parser)
const rawBody = req.body;
// 1. Verify signature
const verification = config.verifier.verify(
rawBody,
req.headers as Record<string, string>
);
if (!verification.valid) {
console.error('Webhook verification failed:', verification.error);
return res.status(401).json({ error: verification.error });
}
// Parse payload
const payload = JSON.parse(rawBody.toString());
// 2. Check idempotency
const eventId = config.eventIdExtractor(payload);
if (await config.idempotency.isProcessed(eventId)) {
console.log(`Webhook ${eventId} already processed, skipping`);
return res.status(200).json({ status: 'already_processed' });
}
// 3. Attach parsed payload and continue
req.body = payload;
(req as any).webhookEventId = eventId;
// 4. After processing, mark as processed
res.on('finish', async () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
await config.idempotency.markProcessed(eventId);
}
});
next();
};
}
export { createWebhookHandler };Python Implementation
Python 实现
python
undefinedpython
undefinedwebhook_security.py
webhook_security.py
import hmac
import hashlib
import time
from typing import Optional, Dict, Any
from dataclasses import dataclass
import redis
@dataclass
class VerificationResult:
valid: bool
error: Optional[str] = None
class WebhookVerifier:
def init(
self,
secret: str,
signature_header: str,
timestamp_header: Optional[str] = None,
tolerance: int = 300,
):
self.secret = secret
self.signature_header = signature_header.lower()
self.timestamp_header = timestamp_header.lower() if timestamp_header else None
self.tolerance = tolerance
def verify(self, payload: bytes, headers: Dict[str, str]) -> VerificationResult:
# Normalize headers to lowercase
headers = {k.lower(): v for k, v in headers.items()}
signature = headers.get(self.signature_header)
if not signature:
return VerificationResult(False, "Missing signature header")
# Check timestamp
if self.timestamp_header:
timestamp = headers.get(self.timestamp_header)
if not timestamp:
return VerificationResult(False, "Missing timestamp header")
timestamp_age = abs(int(time.time()) - int(timestamp))
if timestamp_age > self.tolerance:
return VerificationResult(False, "Timestamp outside tolerance")
# Compute expected signature
expected = self._compute_signature(payload, headers)
# Constant-time comparison
valid = hmac.compare_digest(signature, expected)
return VerificationResult(valid, None if valid else "Invalid signature")
def _compute_signature(self, payload: bytes, headers: Dict[str, str]) -> str:
timestamp = headers.get(self.timestamp_header, "") if self.timestamp_header else ""
signed_payload = f"{timestamp}.{payload.decode()}" if timestamp else payload
if isinstance(signed_payload, str):
signed_payload = signed_payload.encode()
signature = hmac.new(
self.secret.encode(),
signed_payload,
hashlib.sha256
).hexdigest()
return f"sha256={signature}"class IdempotencyHandler:
def init(
self,
redis_client: redis.Redis,
key_prefix: str = "webhook:processed:",
ttl_seconds: int = 86400,
):
self.redis = redis_client
self.key_prefix = key_prefix
self.ttl = ttl_seconds
def is_processed(self, event_id: str) -> bool:
key = f"{self.key_prefix}{event_id}"
return self.redis.exists(key) == 1
def mark_processed(self, event_id: str, result: Any = None) -> None:
import json
key = f"{self.key_prefix}{event_id}"
value = json.dumps({
"processed_at": time.time(),
"result": result,
})
self.redis.setex(key, self.ttl, value)undefinedimport hmac
import hashlib
import time
from typing import Optional, Dict, Any
from dataclasses import dataclass
import redis
@dataclass
class VerificationResult:
valid: bool
error: Optional[str] = None
class WebhookVerifier:
def init(
self,
secret: str,
signature_header: str,
timestamp_header: Optional[str] = None,
tolerance: int = 300,
):
self.secret = secret
self.signature_header = signature_header.lower()
self.timestamp_header = timestamp_header.lower() if timestamp_header else None
self.tolerance = tolerance
def verify(self, payload: bytes, headers: Dict[str, str]) -> VerificationResult:
# Normalize headers to lowercase
headers = {k.lower(): v for k, v in headers.items()}
signature = headers.get(self.signature_header)
if not signature:
return VerificationResult(False, "Missing signature header")
# Check timestamp
if self.timestamp_header:
timestamp = headers.get(self.timestamp_header)
if not timestamp:
return VerificationResult(False, "Missing timestamp header")
timestamp_age = abs(int(time.time()) - int(timestamp))
if timestamp_age > self.tolerance:
return VerificationResult(False, "Timestamp outside tolerance")
# Compute expected signature
expected = self._compute_signature(payload, headers)
# Constant-time comparison
valid = hmac.compare_digest(signature, expected)
return VerificationResult(valid, None if valid else "Invalid signature")
def _compute_signature(self, payload: bytes, headers: Dict[str, str]) -> str:
timestamp = headers.get(self.timestamp_header, "") if self.timestamp_header else ""
signed_payload = f"{timestamp}.{payload.decode()}" if timestamp else payload
if isinstance(signed_payload, str):
signed_payload = signed_payload.encode()
signature = hmac.new(
self.secret.encode(),
signed_payload,
hashlib.sha256
).hexdigest()
return f"sha256={signature}"class IdempotencyHandler:
def init(
self,
redis_client: redis.Redis,
key_prefix: str = "webhook:processed:",
ttl_seconds: int = 86400,
):
self.redis = redis_client
self.key_prefix = key_prefix
self.ttl = ttl_seconds
def is_processed(self, event_id: str) -> bool:
key = f"{self.key_prefix}{event_id}"
return self.redis.exists(key) == 1
def mark_processed(self, event_id: str, result: Any = None) -> None:
import json
key = f"{self.key_prefix}{event_id}"
value = json.dumps({
"processed_at": time.time(),
"result": result,
})
self.redis.setex(key, self.ttl, value)undefinedFastAPI Middleware
FastAPI 中间件
python
undefinedpython
undefinedfastapi_webhook.py
fastapi_webhook.py
from fastapi import Request, HTTPException, Depends
from functools import wraps
def webhook_protected(
verifier: WebhookVerifier,
idempotency: IdempotencyHandler,
event_id_extractor: callable,
):
async def dependency(request: Request):
# Get raw body
body = await request.body()
# Verify signature
result = verifier.verify(body, dict(request.headers))
if not result.valid:
raise HTTPException(status_code=401, detail=result.error)
# Parse and check idempotency
import json
payload = json.loads(body)
event_id = event_id_extractor(payload)
if idempotency.is_processed(event_id):
raise HTTPException(status_code=200, detail="Already processed")
# Store for later marking
request.state.webhook_event_id = event_id
request.state.webhook_payload = payload
return payload
return Depends(dependency)from fastapi import Request, HTTPException, Depends
from functools import wraps
def webhook_protected(
verifier: WebhookVerifier,
idempotency: IdempotencyHandler,
event_id_extractor: callable,
):
async def dependency(request: Request):
# Get raw body
body = await request.body()
# Verify signature
result = verifier.verify(body, dict(request.headers))
if not result.valid:
raise HTTPException(status_code=401, detail=result.error)
# Parse and check idempotency
import json
payload = json.loads(body)
event_id = event_id_extractor(payload)
if idempotency.is_processed(event_id):
raise HTTPException(status_code=200, detail="Already processed")
# Store for later marking
request.state.webhook_event_id = event_id
request.state.webhook_payload = payload
return payload
return Depends(dependency)Usage
Usage
@app.post("/webhooks/stripe")
async def stripe_webhook(
payload: dict = webhook_protected(
verifier=stripe_verifier,
idempotency=idempotency_handler,
event_id_extractor=lambda p: p["id"],
)
):
# Process webhook
event_type = payload["type"]
# ...
# Mark as processed
idempotency_handler.mark_processed(payload["id"])
return {"status": "ok"}undefined@app.post("/webhooks/stripe")
async def stripe_webhook(
payload: dict = webhook_protected(
verifier=stripe_verifier,
idempotency=idempotency_handler,
event_id_extractor=lambda p: p["id"],
)
):
# Process webhook
event_type = payload["type"]
# ...
# Mark as processed
idempotency_handler.mark_processed(payload["id"])
return {"status": "ok"}undefinedExpress Setup
Express 配置
typescript
// Important: Use raw body parser for webhooks
import express from 'express';
const app = express();
// Regular JSON parser for most routes
app.use(express.json());
// Raw body parser for webhook routes
app.use('/webhooks', express.raw({ type: 'application/json' }));
// Webhook route with verification
app.post('/webhooks/stripe',
createWebhookHandler({
verifier: stripeVerifier,
idempotency: idempotencyHandler,
eventIdExtractor: (p: any) => p.id,
}),
async (req, res) => {
const event = req.body;
switch (event.type) {
case 'checkout.session.completed':
await handleCheckout(event.data.object);
break;
// ... other handlers
}
res.json({ received: true });
}
);typescript
// Important: Use raw body parser for webhooks
import express from 'express';
const app = express();
// Regular JSON parser for most routes
app.use(express.json());
// Raw body parser for webhook routes
app.use('/webhooks', express.raw({ type: 'application/json' }));
// Webhook route with verification
app.post('/webhooks/stripe',
createWebhookHandler({
verifier: stripeVerifier,
idempotency: idempotencyHandler,
eventIdExtractor: (p: any) => p.id,
}),
async (req, res) => {
const event = req.body;
switch (event.type) {
case 'checkout.session.completed':
await handleCheckout(event.data.object);
break;
// ... other handlers
}
res.json({ received: true });
}
);Building Your Own Webhook Sender
构建自有Webhook发送器
typescript
// webhook-sender.ts
import crypto from 'crypto';
interface WebhookDelivery {
url: string;
event: string;
payload: unknown;
secret: string;
}
async function sendWebhook(delivery: WebhookDelivery): Promise<boolean> {
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify(delivery.payload);
// Create signature
const signedPayload = `${timestamp}.${body}`;
const signature = 'sha256=' + crypto
.createHmac('sha256', delivery.secret)
.update(signedPayload)
.digest('hex');
try {
const response = await fetch(delivery.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature,
'X-Webhook-Timestamp': timestamp,
'X-Webhook-Event': delivery.event,
'X-Webhook-ID': crypto.randomUUID(),
},
body,
});
return response.ok;
} catch (error) {
console.error('Webhook delivery failed:', error);
return false;
}
}typescript
// webhook-sender.ts
import crypto from 'crypto';
interface WebhookDelivery {
url: string;
event: string;
payload: unknown;
secret: string;
}
async function sendWebhook(delivery: WebhookDelivery): Promise<boolean> {
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify(delivery.payload);
// Create signature
const signedPayload = `${timestamp}.${body}`;
const signature = 'sha256=' + crypto
.createHmac('sha256', delivery.secret)
.update(signedPayload)
.digest('hex');
try {
const response = await fetch(delivery.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature,
'X-Webhook-Timestamp': timestamp,
'X-Webhook-Event': delivery.event,
'X-Webhook-ID': crypto.randomUUID(),
},
body,
});
return response.ok;
} catch (error) {
console.error('Webhook delivery failed:', error);
return false;
}
}Best Practices
最佳实践
- Always verify signatures: Never trust unverified payloads
- Use constant-time comparison: Prevents timing attacks
- Check timestamps: Prevents replay attacks
- Implement idempotency: Same webhook may be delivered multiple times
- Use raw body parser: JSON parsing before verification breaks signatures
- Return 200 quickly: Process async to avoid timeouts
- 始终验证签名:绝不信任未经验证的请求payload
- 使用恒时比较:防止时序攻击
- 验证时间戳:防止重放攻击
- 实现幂等性:同一Webhook可能会被多次分发
- 使用原始Body解析器:验证前解析JSON会破坏签名
- 快速返回200响应:异步处理以避免超时
Common Mistakes
常见错误
- Parsing JSON before signature verification
- Using regular string comparison for signatures
- Not handling duplicate deliveries
- Blocking on webhook processing (causes retries)
- Exposing webhook secrets in logs
- 签名验证前解析JSON
- 使用常规字符串比较验证签名
- 未处理重复分发的请求
- 阻塞式处理Webhook(会导致重试)
- 在日志中暴露Webhook密钥
Security Checklist
安全检查清单
- Signature verification with HMAC-SHA256
- Constant-time signature comparison
- Timestamp validation (5 min tolerance)
- Idempotency handling
- Raw body parser for webhook routes
- Secrets in environment variables
- HTTPS only
- Logging without exposing secrets
- 使用HMAC-SHA256进行签名验证
- 采用恒时比较法验证签名
- 时间戳验证(5分钟容忍窗口)
- 幂等性处理
- Webhook路由使用原始Body解析器
- 密钥存储在环境变量中
- 仅使用HTTPS
- 日志中不暴露密钥