webhook-security

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Webhook 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
undefined
python
undefined

webhook_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)
undefined
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)
undefined

FastAPI Middleware

FastAPI 中间件

python
undefined
python
undefined

fastapi_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"}
undefined

Express 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

最佳实践

  1. Always verify signatures: Never trust unverified payloads
  2. Use constant-time comparison: Prevents timing attacks
  3. Check timestamps: Prevents replay attacks
  4. Implement idempotency: Same webhook may be delivered multiple times
  5. Use raw body parser: JSON parsing before verification breaks signatures
  6. Return 200 quickly: Process async to avoid timeouts
  1. 始终验证签名:绝不信任未经验证的请求payload
  2. 使用恒时比较:防止时序攻击
  3. 验证时间戳:防止重放攻击
  4. 实现幂等性:同一Webhook可能会被多次分发
  5. 使用原始Body解析器:验证前解析JSON会破坏签名
  6. 快速返回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
  • 日志中不暴露密钥