Webhook Security Guide

Learn how to securely handle Stateset webhooks with comprehensive security patterns, signature verification, replay attack prevention, and production-ready implementation examples.

Prerequisites

Before you begin, ensure you have:

  • A Stateset account with webhook endpoints configured
  • Understanding of HMAC signature verification
  • HTTPS endpoint for receiving webhooks
  • Basic knowledge of Node.js/Express (examples provided)
  • Production environment security considerations

Security Fundamentals

1

Webhook Secret Configuration

Configure your webhook secret in the Stateset dashboard:

# .env
STATESET_WEBHOOK_SECRET=whsec_3rK9pL7nQ2xS5mT8...
WEBHOOK_ENDPOINT_URL=https://api.yourstore.com/webhooks/stateset

# Security Settings
WEBHOOK_TIMEOUT_MS=5000
MAX_WEBHOOK_BODY_SIZE=1048576  # 1MB
ENABLE_REPLAY_PROTECTION=true
REPLAY_TOLERANCE_SECONDS=300   # 5 minutes
2

HTTPS Requirements

Ensure your webhook endpoint uses HTTPS:

// webhook-server.js
import express from 'express';
import https from 'https';
import fs from 'fs';

const app = express();

// Production HTTPS setup
if (process.env.NODE_ENV === 'production') {
  const options = {
    key: fs.readFileSync('/path/to/private-key.pem'),
    cert: fs.readFileSync('/path/to/certificate.pem'),
    // Additional security headers
    secureProtocol: 'TLSv1_2_method',
    ciphers: [
      'ECDHE-RSA-AES128-GCM-SHA256',
      'ECDHE-RSA-AES256-GCM-SHA384',
      'ECDHE-RSA-AES128-SHA256',
      'ECDHE-RSA-AES256-SHA384'
    ].join(':'),
    honorCipherOrder: true
  };
  
  https.createServer(options, app).listen(443, () => {
    console.log('Secure webhook server running on port 443');
  });
} else {
  app.listen(3000, () => {
    console.log('Development webhook server running on port 3000');
  });
}
3

Request Validation Middleware

Implement comprehensive request validation:

import rateLimit from 'express-rate-limit';
import helmet from 'helmet';

// Security middleware
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'none'"],
      styleSrc: ["'none'"],
      imgSrc: ["'none'"]
    }
  }
}));

// Rate limiting for webhook endpoints
const webhookLimiter = rateLimit({
  windowMs: 1 * 60 * 1000, // 1 minute
  max: 100, // Limit each IP to 100 requests per windowMs
  message: 'Too many webhook requests from this IP',
  standardHeaders: true,
  legacyHeaders: false,
  skip: (req) => {
    // Skip rate limiting for known Stateset IPs
    const statesetIPs = ['52.89.214.238', '54.187.174.169', '54.187.205.235'];
    return statesetIPs.includes(req.ip);
  }
});

app.use('/webhooks', webhookLimiter);

Signature Verification

Basic HMAC Verification

Implement secure signature verification:

import crypto from 'crypto';
import express from 'express';

class WebhookVerifier {
  constructor(secret) {
    this.secret = secret;
    this.tolerance = 300; // 5 minutes in seconds
  }
  
  verifySignature(payload, signature, timestamp) {
    try {
      // Verify timestamp to prevent replay attacks
      const currentTime = Math.floor(Date.now() / 1000);
      if (Math.abs(currentTime - timestamp) > this.tolerance) {
        throw new Error('Webhook timestamp is too old');
      }
      
      // Create expected signature
      const expectedSignature = this.computeSignature(payload, timestamp);
      
      // Use constant-time comparison to prevent timing attacks
      return this.secureCompare(signature, expectedSignature);
      
    } catch (error) {
      console.error('Signature verification failed:', error.message);
      return false;
    }
  }
  
  computeSignature(payload, timestamp) {
    const signedPayload = `${timestamp}.${payload}`;
    return crypto
      .createHmac('sha256', this.secret)
      .update(signedPayload, 'utf8')
      .digest('hex');
  }
  
  secureCompare(a, b) {
    if (a.length !== b.length) {
      return false;
    }
    
    let result = 0;
    for (let i = 0; i < a.length; i++) {
      result |= a.charCodeAt(i) ^ b.charCodeAt(i);
    }
    
    return result === 0;
  }
  
  extractSignature(header) {
    const elements = header.split(',');
    const signatures = {};
    
    for (const element of elements) {
      const [key, value] = element.split('=');
      if (key === 'v1') {
        signatures.v1 = value;
      } else if (key === 't') {
        signatures.timestamp = parseInt(value, 10);
      }
    }
    
    return signatures;
  }
}

// Initialize verifier
const verifier = new WebhookVerifier(process.env.STATESET_WEBHOOK_SECRET);

// Middleware to capture raw body
app.use('/webhooks/stateset', express.raw({ type: 'application/json', limit: '1mb' }));

// Webhook verification middleware
function verifyWebhook(req, res, next) {
  const signature = req.headers['stateset-signature'];
  const payload = req.body.toString();
  
  if (!signature) {
    return res.status(401).json({ error: 'Missing signature header' });
  }
  
  try {
    const { v1: sig, timestamp } = verifier.extractSignature(signature);
    
    if (!sig || !timestamp) {
      return res.status(401).json({ error: 'Invalid signature format' });
    }
    
    const isValid = verifier.verifySignature(payload, sig, timestamp);
    
    if (!isValid) {
      return res.status(401).json({ error: 'Invalid signature' });
    }
    
    // Parse JSON only after verification
    req.body = JSON.parse(payload);
    next();
    
  } catch (error) {
    console.error('Webhook verification error:', error);
    return res.status(400).json({ error: 'Webhook verification failed' });
  }
}

Advanced Signature Verification

Production-ready implementation with enhanced security:

class AdvancedWebhookVerifier extends WebhookVerifier {
  constructor(secret, options = {}) {
    super(secret);
    this.tolerance = options.tolerance || 300;
    this.enableReplayProtection = options.enableReplayProtection !== false;
    this.processedWebhooks = new Map(); // In production, use Redis
    this.maxCacheSize = options.maxCacheSize || 10000;
  }
  
  async verifyWebhookWithReplayProtection(payload, signature, timestamp, webhookId) {
    // Check for replay attacks
    if (this.enableReplayProtection && this.processedWebhooks.has(webhookId)) {
      throw new Error('Webhook has already been processed (replay attack detected)');
    }
    
    // Verify signature
    const isValid = this.verifySignature(payload, signature, timestamp);
    
    if (isValid && this.enableReplayProtection) {
      // Store webhook ID to prevent replay
      this.processedWebhooks.set(webhookId, Date.now());
      
      // Clean up old entries to prevent memory leaks
      this.cleanupCache();
    }
    
    return isValid;
  }
  
  cleanupCache() {
    if (this.processedWebhooks.size > this.maxCacheSize) {
      const cutoff = Date.now() - (this.tolerance * 1000);
      
      for (const [id, timestamp] of this.processedWebhooks.entries()) {
        if (timestamp < cutoff) {
          this.processedWebhooks.delete(id);
        }
      }
    }
  }
  
  // Verify multiple signature versions for backward compatibility
  verifyMultipleSignatures(payload, signatureHeader, timestamp) {
    const signatures = this.extractSignature(signatureHeader);
    
    // Try v1 signature first
    if (signatures.v1) {
      const isValid = this.secureCompare(
        signatures.v1,
        this.computeSignature(payload, timestamp)
      );
      if (isValid) return true;
    }
    
    // Fallback to other versions if needed
    // This allows for graceful migration between signature versions
    
    return false;
  }
}

// Production webhook handler
const advancedVerifier = new AdvancedWebhookVerifier(
  process.env.STATESET_WEBHOOK_SECRET,
  {
    tolerance: 300,
    enableReplayProtection: true,
    maxCacheSize: 10000
  }
);

async function advancedVerifyWebhook(req, res, next) {
  const signature = req.headers['stateset-signature'];
  const webhookId = req.headers['stateset-webhook-id'];
  const payload = req.body.toString();
  
  if (!signature || !webhookId) {
    return res.status(401).json({ 
      error: 'Missing required headers',
      required: ['stateset-signature', 'stateset-webhook-id']
    });
  }
  
  try {
    const { v1: sig, timestamp } = advancedVerifier.extractSignature(signature);
    
    const isValid = await advancedVerifier.verifyWebhookWithReplayProtection(
      payload, 
      sig, 
      timestamp, 
      webhookId
    );
    
    if (!isValid) {
      // Log security event
      console.warn('Webhook security violation:', {
        ip: req.ip,
        userAgent: req.headers['user-agent'],
        timestamp: new Date().toISOString(),
        webhookId,
        signatureProvided: !!signature
      });
      
      return res.status(401).json({ error: 'Webhook verification failed' });
    }
    
    req.body = JSON.parse(payload);
    req.webhookId = webhookId;
    next();
    
  } catch (error) {
    console.error('Advanced webhook verification error:', error);
    return res.status(400).json({ error: error.message });
  }
}

Event Processing Security

Secure Event Handler

Implement secure and resilient event processing:

class SecureWebhookProcessor {
  constructor() {
    this.eventHandlers = new Map();
    this.processingQueue = [];
    this.maxRetries = 3;
    this.retryDelays = [1000, 5000, 15000]; // Progressive backoff
  }
  
  registerHandler(eventType, handler, options = {}) {
    this.eventHandlers.set(eventType, {
      handler,
      requiresAuth: options.requiresAuth !== false,
      timeout: options.timeout || 30000,
      retryable: options.retryable !== false
    });
  }
  
  async processWebhook(webhookData, context = {}) {
    const { event, data, id: webhookId } = webhookData;
    
    // Input validation
    if (!event || !data) {
      throw new Error('Invalid webhook payload structure');
    }
    
    const handlerConfig = this.eventHandlers.get(event);
    
    if (!handlerConfig) {
      console.warn(`No handler registered for event: ${event}`);
      return { status: 'ignored', reason: 'no_handler' };
    }
    
    // Security context validation
    if (handlerConfig.requiresAuth && !context.authenticated) {
      throw new Error('Authentication required for this event type');
    }
    
    return await this.executeWithRetry(
      handlerConfig,
      data,
      { ...context, event, webhookId }
    );
  }
  
  async executeWithRetry(handlerConfig, data, context) {
    let lastError;
    
    for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
      try {
        // Set timeout for handler execution
        const result = await Promise.race([
          handlerConfig.handler(data, context),
          this.timeoutPromise(handlerConfig.timeout)
        ]);
        
        return { 
          status: 'success', 
          result, 
          attempt: attempt + 1 
        };
        
      } catch (error) {
        lastError = error;
        
        console.error(`Webhook handler failed (attempt ${attempt + 1}):`, {
          event: context.event,
          webhookId: context.webhookId,
          error: error.message,
          stack: error.stack
        });
        
        // Don't retry certain types of errors
        if (!handlerConfig.retryable || this.isNonRetryableError(error)) {
          break;
        }
        
        // Wait before retrying (except on last attempt)
        if (attempt < this.maxRetries) {
          await this.delay(this.retryDelays[attempt] || 15000);
        }
      }
    }
    
    throw new Error(
      `Webhook processing failed after ${this.maxRetries + 1} attempts: ${lastError.message}`
    );
  }
  
  timeoutPromise(timeout) {
    return new Promise((_, reject) => {
      setTimeout(() => reject(new Error('Handler timeout')), timeout);
    });
  }
  
  isNonRetryableError(error) {
    const nonRetryablePatterns = [
      /validation/i,
      /authentication/i,
      /authorization/i,
      /not found/i,
      /duplicate/i
    ];
    
    return nonRetryablePatterns.some(pattern => 
      pattern.test(error.message)
    );
  }
  
  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// Initialize processor and register handlers
const processor = new SecureWebhookProcessor();

// Order event handlers
processor.registerHandler('order.created', async (orderData, context) => {
  // Validate order data
  if (!orderData.id || !orderData.customer_email) {
    throw new Error('Invalid order data: missing required fields');
  }
  
  // Process order creation
  console.log(`Processing order creation: ${orderData.id}`);
  
  // Your business logic here
  await processNewOrder(orderData);
  
  return { processed: true, orderId: orderData.id };
}, { timeout: 30000 });

processor.registerHandler('order.cancelled', async (orderData, context) => {
  // Handle order cancellation
  console.log(`Processing order cancellation: ${orderData.id}`);
  
  await processOrderCancellation(orderData);
  
  return { processed: true, orderId: orderData.id };
});

processor.registerHandler('payment.failed', async (paymentData, context) => {
  // Handle payment failures (non-retryable)
  console.log(`Processing payment failure: ${paymentData.payment_id}`);
  
  await handlePaymentFailure(paymentData);
  
  return { processed: true, paymentId: paymentData.payment_id };
}, { retryable: false });

Complete Webhook Endpoint

Production-ready webhook endpoint with all security measures:

// Complete secure webhook endpoint
app.post('/webhooks/stateset', 
  advancedVerifyWebhook,
  async (req, res) => {
    const startTime = Date.now();
    
    try {
      // Add request context
      const context = {
        ip: req.ip,
        userAgent: req.headers['user-agent'],
        timestamp: new Date().toISOString(),
        authenticated: true, // Set by verification middleware
        webhookId: req.webhookId
      };
      
      // Process webhook
      const result = await processor.processWebhook(req.body, context);
      
      // Log successful processing
      console.log('Webhook processed successfully:', {
        event: req.body.event,
        webhookId: req.webhookId,
        processingTime: Date.now() - startTime,
        result: result.status
      });
      
      // Return success response
      res.status(200).json({
        received: true,
        processed: result.status === 'success',
        webhookId: req.webhookId,
        processingTime: Date.now() - startTime
      });
      
    } catch (error) {
      // Log error with context
      console.error('Webhook processing error:', {
        event: req.body?.event,
        webhookId: req.webhookId,
        error: error.message,
        stack: error.stack,
        processingTime: Date.now() - startTime
      });
      
      // Return appropriate error response
      const statusCode = error.message.includes('authentication') ? 401 :
                        error.message.includes('validation') ? 400 : 500;
      
      res.status(statusCode).json({
        received: true,
        processed: false,
        error: error.message,
        webhookId: req.webhookId
      });
    }
  }
);

IP Allowlisting

Stateset IP Ranges

Configure IP allowlisting for enhanced security:

class IPAllowlist {
  constructor() {
    // Stateset webhook IP ranges (update as needed)
    this.allowedRanges = [
      '52.89.214.238/32',
      '54.187.174.169/32', 
      '54.187.205.235/32',
      // Add more Stateset IP ranges as provided
    ];
    
    this.compiledRanges = this.compileRanges();
  }
  
  compileRanges() {
    return this.allowedRanges.map(range => {
      const [ip, cidr] = range.split('/');
      const mask = ~(0xFFFFFFFF >>> parseInt(cidr, 10));
      return {
        network: this.ipToInt(ip) & mask,
        mask: mask
      };
    });
  }
  
  ipToInt(ip) {
    return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
  }
  
  isAllowed(ip) {
    const ipInt = this.ipToInt(ip);
    
    return this.compiledRanges.some(range => 
      (ipInt & range.mask) === range.network
    );
  }
}

// IP allowlist middleware
const ipAllowlist = new IPAllowlist();

function checkIPAllowlist(req, res, next) {
  // Skip in development
  if (process.env.NODE_ENV !== 'production') {
    return next();
  }
  
  const clientIP = req.ip || req.connection.remoteAddress;
  
  if (!ipAllowlist.isAllowed(clientIP)) {
    console.warn('Webhook request from unauthorized IP:', {
      ip: clientIP,
      userAgent: req.headers['user-agent'],
      timestamp: new Date().toISOString()
    });
    
    return res.status(403).json({ 
      error: 'IP not allowed',
      ip: clientIP 
    });
  }
  
  next();
}

// Apply IP allowlist before webhook processing
app.use('/webhooks/stateset', checkIPAllowlist);

Monitoring and Alerting

Security Event Monitoring

Implement comprehensive security monitoring:

import winston from 'winston';

class WebhookSecurityMonitor {
  constructor() {
    this.logger = winston.createLogger({
      level: 'info',
      format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
      ),
      transports: [
        new winston.transports.File({ filename: 'webhook-security.log' }),
        new winston.transports.Console()
      ]
    });
    
    this.securityEvents = new Map();
    this.alertThresholds = {
      failedVerifications: 10,
      suspiciousIPs: 5,
      timeWindow: 300000 // 5 minutes
    };
  }
  
  logSecurityEvent(eventType, data) {
    const event = {
      type: eventType,
      timestamp: Date.now(),
      data: data
    };
    
    this.logger.warn('Security event detected', event);
    
    // Track events for alerting
    this.trackEventForAlerting(eventType, data);
  }
  
  trackEventForAlerting(eventType, data) {
    const key = `${eventType}:${data.ip || 'unknown'}`;
    const now = Date.now();
    
    if (!this.securityEvents.has(key)) {
      this.securityEvents.set(key, []);
    }
    
    const events = this.securityEvents.get(key);
    events.push(now);
    
    // Clean old events
    const cutoff = now - this.alertThresholds.timeWindow;
    const recentEvents = events.filter(timestamp => timestamp > cutoff);
    this.securityEvents.set(key, recentEvents);
    
    // Check for alert conditions
    this.checkAlertConditions(eventType, data, recentEvents.length);
  }
  
  checkAlertConditions(eventType, data, eventCount) {
    let shouldAlert = false;
    
    if (eventType === 'signature_verification_failed' && 
        eventCount >= this.alertThresholds.failedVerifications) {
      shouldAlert = true;
    }
    
    if (eventType === 'suspicious_ip' && 
        eventCount >= this.alertThresholds.suspiciousIPs) {
      shouldAlert = true;
    }
    
    if (shouldAlert) {
      this.sendSecurityAlert(eventType, data, eventCount);
    }
  }
  
  async sendSecurityAlert(eventType, data, eventCount) {
    const alert = {
      type: 'webhook_security_alert',
      eventType,
      count: eventCount,
      timeWindow: this.alertThresholds.timeWindow / 1000 / 60, // minutes
      data,
      timestamp: new Date().toISOString()
    };
    
    this.logger.error('Security alert triggered', alert);
    
    // Send to monitoring service (PagerDuty, Slack, etc.)
    try {
      await this.sendToMonitoringService(alert);
    } catch (error) {
      console.error('Failed to send security alert:', error);
    }
  }
  
  async sendToMonitoringService(alert) {
    // Example: Send to Slack webhook
    if (process.env.SLACK_WEBHOOK_URL) {
      const message = {
        text: `🚨 Webhook Security Alert: ${alert.eventType}`,
        attachments: [{
          color: 'danger',
          fields: [
            { title: 'Event Type', value: alert.eventType, short: true },
            { title: 'Count', value: alert.count, short: true },
            { title: 'Time Window', value: `${alert.timeWindow} minutes`, short: true },
            { title: 'IP Address', value: alert.data.ip || 'Unknown', short: true }
          ],
          timestamp: Math.floor(Date.now() / 1000)
        }]
      };
      
      // Send to Slack (implement HTTP request)
      // await fetch(process.env.SLACK_WEBHOOK_URL, { ... });
    }
  }
}

// Initialize security monitor
const securityMonitor = new WebhookSecurityMonitor();

// Enhanced verification middleware with monitoring
function monitoredVerifyWebhook(req, res, next) {
  const signature = req.headers['stateset-signature'];
  const webhookId = req.headers['stateset-webhook-id'];
  const payload = req.body.toString();
  const clientIP = req.ip;
  
  if (!signature || !webhookId) {
    securityMonitor.logSecurityEvent('missing_headers', {
      ip: clientIP,
      userAgent: req.headers['user-agent'],
      missingHeaders: [
        !signature && 'stateset-signature',
        !webhookId && 'stateset-webhook-id'
      ].filter(Boolean)
    });
    
    return res.status(401).json({ 
      error: 'Missing required headers' 
    });
  }
  
  try {
    const { v1: sig, timestamp } = advancedVerifier.extractSignature(signature);
    
    const isValid = advancedVerifier.verifySignature(payload, sig, timestamp);
    
    if (!isValid) {
      securityMonitor.logSecurityEvent('signature_verification_failed', {
        ip: clientIP,
        userAgent: req.headers['user-agent'],
        webhookId,
        hasSignature: !!signature,
        hasTimestamp: !!timestamp
      });
      
      return res.status(401).json({ error: 'Invalid signature' });
    }
    
    req.body = JSON.parse(payload);
    req.webhookId = webhookId;
    next();
    
  } catch (error) {
    securityMonitor.logSecurityEvent('verification_error', {
      ip: clientIP,
      error: error.message,
      webhookId
    });
    
    return res.status(400).json({ error: 'Webhook verification failed' });
  }
}

Testing Webhook Security

Security Test Suite

Comprehensive test suite for webhook security:

// webhook-security.test.js
import crypto from 'crypto';
import request from 'supertest';
import app from '../webhook-server.js';

describe('Webhook Security', () => {
  const webhookSecret = 'test_webhook_secret';
  
  function createValidSignature(payload, timestamp) {
    const signedPayload = `${timestamp}.${payload}`;
    const signature = crypto
      .createHmac('sha256', webhookSecret)
      .update(signedPayload, 'utf8')
      .digest('hex');
    return `t=${timestamp},v1=${signature}`;
  }
  
  describe('Signature Verification', () => {
    it('should accept valid signatures', async () => {
      const payload = JSON.stringify({ event: 'order.created', data: { id: '123' } });
      const timestamp = Math.floor(Date.now() / 1000);
      const signature = createValidSignature(payload, timestamp);
      
      const response = await request(app)
        .post('/webhooks/stateset')
        .set('stateset-signature', signature)
        .set('stateset-webhook-id', 'test-webhook-id')
        .send(payload)
        .expect(200);
      
      expect(response.body.received).toBe(true);
    });
    
    it('should reject invalid signatures', async () => {
      const payload = JSON.stringify({ event: 'order.created', data: { id: '123' } });
      
      await request(app)
        .post('/webhooks/stateset')
        .set('stateset-signature', 'invalid-signature')
        .set('stateset-webhook-id', 'test-webhook-id')
        .send(payload)
        .expect(401);
    });
    
    it('should reject old timestamps', async () => {
      const payload = JSON.stringify({ event: 'order.created', data: { id: '123' } });
      const oldTimestamp = Math.floor(Date.now() / 1000) - 600; // 10 minutes ago
      const signature = createValidSignature(payload, oldTimestamp);
      
      await request(app)
        .post('/webhooks/stateset')
        .set('stateset-signature', signature)
        .set('stateset-webhook-id', 'test-webhook-id')
        .send(payload)
        .expect(401);
    });
    
    it('should prevent replay attacks', async () => {
      const payload = JSON.stringify({ event: 'order.created', data: { id: '123' } });
      const timestamp = Math.floor(Date.now() / 1000);
      const signature = createValidSignature(payload, timestamp);
      const webhookId = 'unique-webhook-id';
      
      // First request should succeed
      await request(app)
        .post('/webhooks/stateset')
        .set('stateset-signature', signature)
        .set('stateset-webhook-id', webhookId)
        .send(payload)
        .expect(200);
      
      // Second request with same ID should fail
      await request(app)
        .post('/webhooks/stateset')
        .set('stateset-signature', signature)
        .set('stateset-webhook-id', webhookId)
        .send(payload)
        .expect(401);
    });
  });
  
  describe('Rate Limiting', () => {
    it('should rate limit excessive requests', async () => {
      const payload = JSON.stringify({ event: 'test.event', data: {} });
      const timestamp = Math.floor(Date.now() / 1000);
      
      // Make many requests quickly
      const requests = Array.from({ length: 150 }, (_, i) => {
        const signature = createValidSignature(payload, timestamp);
        return request(app)
          .post('/webhooks/stateset')
          .set('stateset-signature', signature)
          .set('stateset-webhook-id', `webhook-${i}`)
          .send(payload);
      });
      
      const responses = await Promise.allSettled(requests);
      const rateLimited = responses.filter(r => 
        r.status === 'fulfilled' && r.value.status === 429
      );
      
      expect(rateLimited.length).toBeGreaterThan(0);
    });
  });
  
  describe('Input Validation', () => {
    it('should reject oversized payloads', async () => {
      const largePayload = 'x'.repeat(2 * 1024 * 1024); // 2MB
      const timestamp = Math.floor(Date.now() / 1000);
      const signature = createValidSignature(largePayload, timestamp);
      
      await request(app)
        .post('/webhooks/stateset')
        .set('stateset-signature', signature)
        .set('stateset-webhook-id', 'test-webhook-id')
        .send(largePayload)
        .expect(413); // Payload too large
    });
    
    it('should reject malformed JSON', async () => {
      const invalidJson = '{ invalid json }';
      const timestamp = Math.floor(Date.now() / 1000);
      const signature = createValidSignature(invalidJson, timestamp);
      
      await request(app)
        .post('/webhooks/stateset')
        .set('stateset-signature', signature)
        .set('stateset-webhook-id', 'test-webhook-id')
        .send(invalidJson)
        .expect(400);
    });
  });
});

Best Practices Summary

Security Checklist

Production Deployment

# Environment variables for production
STATESET_WEBHOOK_SECRET=whsec_prod_9K2xL5pN...
WEBHOOK_TIMEOUT_MS=30000
MAX_WEBHOOK_BODY_SIZE=1048576
ENABLE_REPLAY_PROTECTION=true
REPLAY_TOLERANCE_SECONDS=300

# Security headers
ENABLE_HELMET=true
ENABLE_RATE_LIMITING=true
ALLOWED_IPS=52.89.214.238,54.187.174.169,54.187.205.235

# Monitoring
ENABLE_SECURITY_MONITORING=true
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/your/slack/webhook
ALERT_EMAIL=security@yourstore.com

# SSL/TLS
SSL_CERT_PATH=/path/to/ssl/cert.pem
SSL_KEY_PATH=/path/to/ssl/private-key.pem

Next Steps

Conclusion

Implementing robust webhook security is critical for protecting your application and ensuring data integrity. This guide provides comprehensive security patterns including signature verification, replay protection, IP allowlisting, and monitoring.

Key takeaways:

  • ✅ Always verify webhook signatures using HMAC-SHA256
  • ✅ Implement replay attack prevention with timestamp validation
  • ✅ Use HTTPS and IP allowlisting for network security
  • ✅ Monitor security events and set up alerting
  • ✅ Test your security implementation thoroughly

With these security measures in place, you can confidently process Stateset webhooks while maintaining the highest security standards.