@leakguard/webhook
v3.0.2
Published
Secure webhook verification with minimal overhead
Maintainers
Readme
@leakguard/webhook
🔒 Secure webhook verification with minimal overhead
Verify incoming webhooks from providers like Stripe, GitHub, Zapier, and more with production-grade security and framework-agnostic design.
Why @leakguard/webhook?
The Problem
Webhooks are a critical attack vector. Without proper verification:
- Spoofed requests can trigger unauthorized actions
- Replay attacks can cause duplicate processing
- Unauthorized access to webhook endpoints
- Data integrity cannot be guaranteed
Why It's Hard
- Cryptographic complexity: HMAC verification, timing attacks, signature formats
- Provider variations: Each service has different header formats and algorithms
- Framework integration: Request body handling varies across Express, Fastify, NestJS
- Edge cases: Encoding issues, missing headers, malformed signatures
Why Not Roll Your Own?
// ❌ Vulnerable to timing attacks
if (providedSignature !== expectedSignature) {
return res.status(401).send('Invalid');
}
// ❌ Improper signature parsing
const signature = header.split('=')[1]; // Breaks on complex formats
// ❌ Missing replay protection
// No timestamp validation or duplicate detection
// ✅ LeakGuard handles all of this correctlyInstallation
npm install @leakguard/webhook
# Peer dependencies for specific frameworks
npm install express @types/express # For Express
npm install fastify # For Fastify
npm install @nestjs/common @nestjs/core # For NestJSQuick Start
Express
import express from 'express';
import { createStripeStrategy, expressWebhookVerify, expressRawBody } from '@leakguard/webhook';
const app = express();
// Important: Use rawBody middleware for HMAC verification
app.use('/webhooks', expressRawBody());
// Verify Stripe webhooks
app.use('/webhooks/stripe', expressWebhookVerify({
strategies: [createStripeStrategy(process.env.STRIPE_WEBHOOK_SECRET!)]
}));
app.post('/webhooks/stripe', (req, res) => {
// Webhook is verified ✅
const event = req.body;
console.log('Verified Stripe event:', event.type);
res.sendStatus(200);
});NestJS
import { Controller, Post, UseGuards, MiddlewareConsumer, Module } from '@nestjs/common';
import {
WebhookVerificationGuard,
RawBodyMiddleware,
createStripeStrategy
} from '@leakguard/webhook';
@Controller('webhooks')
export class WebhookController {
@Post('stripe')
@UseGuards(new WebhookVerificationGuard({
strategies: [createStripeStrategy(process.env.STRIPE_WEBHOOK_SECRET!)]
}))
handleStripeWebhook(@Req() req: Request) {
// Webhook is verified ✅
console.log('Verification metadata:', req.webhookVerification);
return { received: true };
}
}
@Module({
controllers: [WebhookController]
})
export class WebhookModule {
configure(consumer: MiddlewareConsumer) {
// Apply raw body middleware to webhook routes
consumer.apply(RawBodyMiddleware).forRoutes('webhooks/*');
}
}Fastify
import fastify from 'fastify';
import { fastifyWebhookVerify, createGitHubStrategy } from '@leakguard/webhook';
const app = fastify();
// Register webhook verification plugin
await app.register(fastifyWebhookVerify, {
webhook: {
strategies: [createGitHubStrategy(process.env.GITHUB_WEBHOOK_SECRET!)],
routes: ['/webhooks/github']
}
});
app.post('/webhooks/github', async (request, reply) => {
// Webhook is verified ✅
const payload = request.body;
console.log('GitHub event:', payload);
return { received: true };
});Verification Strategies
HMAC Signature Verification
Verify cryptographic signatures using HMAC algorithms:
import { HMACStrategy } from '@leakguard/webhook';
const strategy = new HMACStrategy({
secret: 'your-webhook-secret',
algorithm: 'sha256', // sha1, sha256, sha512
headerName: 'x-signature-256',
signaturePrefix: 'sha256=',
tolerance: 300 // 5 minutes timestamp tolerance
});Token-based Verification
Verify static tokens or API keys:
import { TokenStrategy } from '@leakguard/webhook';
const strategy = new TokenStrategy({
token: 'your-webhook-token',
headerName: 'authorization', // or 'x-api-key', etc.
queryParam: 'token' // Optional: also check query params
});
// Supports Bearer token format
// Header: "Authorization: Bearer your-webhook-token"IP Allowlist Verification
Restrict webhooks to specific IP addresses or CIDR ranges:
import { IPWhitelistStrategy } from '@leakguard/webhook';
const strategy = new IPWhitelistStrategy({
allowedIPs: [
'192.168.1.1', // Single IP
'10.0.0.0/8', // CIDR range
'172.16.0.0/12'
],
trustProxy: true // Trust X-Forwarded-For headers
});Custom Verification
Implement your own verification logic:
import { CustomStrategy } from '@leakguard/webhook';
const strategy = new CustomStrategy(async (request) => {
// Your custom logic here
const isValid = await yourVerificationLogic(request);
return {
verified: isValid,
error: isValid ? undefined : 'Custom verification failed',
metadata: { customField: 'value' }
};
});Provider Presets
Pre-configured strategies for popular webhook providers:
Stripe
import { createStripeStrategy } from '@leakguard/webhook';
const strategy = createStripeStrategy(
process.env.STRIPE_WEBHOOK_SECRET!,
300 // Optional: timestamp tolerance in seconds
);
// Handles Stripe's signature format:
// stripe-signature: t=1640995200,v1=abc123...GitHub
import {
createGitHubStrategy,
createGitHubLegacyStrategy
} from '@leakguard/webhook';
// Modern GitHub webhooks (SHA-256)
const strategy = createGitHubStrategy(process.env.GITHUB_WEBHOOK_SECRET!);
// Legacy GitHub webhooks (SHA-1)
const legacyStrategy = createGitHubLegacyStrategy(process.env.GITHUB_WEBHOOK_SECRET!);Zapier
import { createZapierStrategy } from '@leakguard/webhook';
const strategy = createZapierStrategy(process.env.ZAPIER_TOKEN!);Webflow
import { createWebflowStrategy } from '@leakguard/webhook';
const strategy = createWebflowStrategy(process.env.WEBFLOW_SECRET!);Slack
import { createSlackStrategy } from '@leakguard/webhook';
const strategy = createSlackStrategy(
process.env.SLACK_SIGNING_SECRET!,
300 // Timestamp tolerance
);Discord & Twilio (IP-based)
import { createDiscordStrategy, createTwilioStrategy } from '@leakguard/webhook';
// Discord webhooks (IP allowlist)
const discordStrategy = createDiscordStrategy();
// Twilio webhooks (IP allowlist)
const twilioStrategy = createTwilioStrategy();Shopify & E-commerce
import { createShopifyStrategy, createSquareStrategy, createLemonSqueezyStrategy } from '@leakguard/webhook';
// Shopify webhooks
const shopifyStrategy = createShopifyStrategy(process.env.SHOPIFY_SECRET!);
// Square webhooks
const squareStrategy = createSquareStrategy(process.env.SQUARE_SIGNATURE_KEY!);
// LemonSqueezy webhooks
const lemonSqueezyStrategy = createLemonSqueezyStrategy(process.env.LEMONSQUEEZY_SECRET!);Developer & DevOps Tools
import {
createGitLabStrategy,
createBitbucketStrategy,
createCircleCIStrategy,
createVercelStrategy,
createNetlifyStrategy
} from '@leakguard/webhook';
// GitLab webhooks
const gitlabStrategy = createGitLabStrategy(process.env.GITLAB_TOKEN!);
// Bitbucket webhooks
const bitbucketStrategy = createBitbucketStrategy(process.env.BITBUCKET_SECRET!);
// CircleCI webhooks
const circleciStrategy = createCircleCIStrategy(process.env.CIRCLECI_SECRET!);
// Vercel webhooks
const vercelStrategy = createVercelStrategy(process.env.VERCEL_SECRET!);
// Netlify webhooks
const netlifyStrategy = createNetlifyStrategy(process.env.NETLIFY_SECRET!);Productivity & CRM Tools
import {
createLinearStrategy,
createNotionStrategy,
createAsanaStrategy,
createAirtableStrategy,
createCalendlyStrategy,
createFigmaStrategy
} from '@leakguard/webhook';
// Linear webhooks
const linearStrategy = createLinearStrategy(process.env.LINEAR_SECRET!);
// Notion webhooks
const notionStrategy = createNotionStrategy(process.env.NOTION_SECRET!);
// Asana webhooks
const asanaStrategy = createAsanaStrategy(process.env.ASANA_SECRET!);
// Airtable webhooks
const airtableStrategy = createAirtableStrategy(process.env.AIRTABLE_SECRET!);
// Calendly webhooks
const calendlyStrategy = createCalendlyStrategy(process.env.CALENDLY_SECRET!);
// Figma webhooks
const figmaStrategy = createFigmaStrategy(process.env.FIGMA_SECRET!);Email & Communication
import {
createSendGridStrategy,
createMailgunStrategy,
createIntercomStrategy,
createHubSpotStrategy
} from '@leakguard/webhook';
// SendGrid webhooks
const sendgridStrategy = createSendGridStrategy(process.env.SENDGRID_SECRET!);
// Mailgun webhooks
const mailgunStrategy = createMailgunStrategy(process.env.MAILGUN_SIGNING_KEY!);
// Intercom webhooks
const intercomStrategy = createIntercomStrategy(process.env.INTERCOM_SECRET!);
// HubSpot webhooks
const hubspotStrategy = createHubSpotStrategy(process.env.HUBSPOT_SECRET!);Enterprise & Support
import {
createSalesforceStrategy,
createZendeskStrategy,
createDocuSignStrategy,
createDropboxStrategy,
createBoxStrategy
} from '@leakguard/webhook';
// Salesforce webhooks
const salesforceStrategy = createSalesforceStrategy(process.env.SALESFORCE_SECRET!);
// Zendesk webhooks
const zendeskStrategy = createZendeskStrategy(process.env.ZENDESK_SECRET!);
// DocuSign webhooks
const docusignStrategy = createDocuSignStrategy(process.env.DOCUSIGN_SECRET!);
// Dropbox webhooks
const dropboxStrategy = createDropboxStrategy(process.env.DROPBOX_SECRET!);
// Box webhooks
const boxStrategy = createBoxStrategy(process.env.BOX_SECRET!);Multiple Providers
import { createProviderStrategies } from '@leakguard/webhook';
const strategies = createProviderStrategies({
stripe: { secret: process.env.STRIPE_SECRET! },
github: { secret: process.env.GITHUB_SECRET! },
shopify: { secret: process.env.SHOPIFY_SECRET! },
linear: { secret: process.env.LINEAR_SECRET! },
sendgrid: { secret: process.env.SENDGRID_SECRET! },
vercel: { secret: process.env.VERCEL_SECRET! }
});Configuration Options
Multiple Strategies
Combine multiple verification strategies:
const config = {
strategies: [
createStripeStrategy(stripeSecret),
new TokenStrategy({ token: backupToken }),
new IPWhitelistStrategy({ allowedIPs: ['trusted-ip'] })
],
// Require ALL strategies to pass (default: false)
requireAll: true,
// Stateless timestamp validation (mitigates many replay attacks)
timestampValidation: {
timestampHeader: 'x-timestamp',
tolerance: 300,
timestampFormat: 'unix' // or 'iso'
},
// Event handlers
onSuccess: (request) => {
console.log('Webhook verified successfully');
},
onFailure: (result, request) => {
console.error('Webhook verification failed:', result.error);
}
};Framework-Specific Options
Express Options
const expressConfig = {
strategies: [/* your strategies */],
skipRoutes: ['/health', '/metrics'], // Skip verification
skipMethods: ['GET', 'OPTIONS'], // Skip HTTP methods
onFailure: (error, req, res, next) => {
// Custom error handling
res.status(401).json({
error: 'Webhook verification failed',
details: error
});
},
onSuccess: (req, res, next) => {
// Custom success handling
req.customField = 'verified';
next();
}
};NestJS Options
const nestConfig = {
strategies: [/* your strategies */],
skipRoutes: ['/health'],
skipMethods: ['GET']
};
// Use as Guard
@UseGuards(new WebhookVerificationGuard(nestConfig))
// Use as Middleware
export class ConfiguredMiddleware extends WebhookVerificationMiddleware {
constructor() {
super(nestConfig);
}
}Fastify Options
const fastifyConfig = {
webhook: {
strategies: [/* your strategies */],
routes: ['/webhooks/*'], // Apply only to specific routes
onFailure: (error, request, reply) => {
reply.code(401).send({ error });
},
onSuccess: (request, reply) => {
// Custom success logic
}
}
};Stateless Security Architecture
Why Stateless?
This package is designed for modern deployment patterns:
- ✅ Serverless functions (AWS Lambda, Vercel, Cloudflare Workers)
- ✅ Kubernetes autoscaling with ephemeral pods
- ✅ Docker containers without persistent storage
- ✅ Multi-region deployments without shared state
Replay Protection Strategy
What we provide ✅
- Timestamp validation: Rejects requests older than X seconds
- Signature verification: Cryptographic integrity protection
- IP allowlisting: Restrict to known sources
- Timing-safe comparisons: Prevent timing attacks
const config = {
strategies: [createStripeStrategy(secret)],
// Stateless replay mitigation
timestampValidation: {
timestampHeader: 'x-timestamp',
tolerance: 300, // 5 minutes - adjust based on your needs
timestampFormat: 'unix'
}
};What we DON'T provide ❌
- Perfect replay protection: Requires external storage (Redis/DB)
- Request deduplication: Would break stateless architecture
- Global nonce tracking: Incompatible with autoscaling
When You Need Full Replay Protection
If your application requires perfect replay protection:
// Option 1: Add your own request tracking
const processedRequests = new Set(); // Or Redis/DB
app.use('/webhook', (req, res, next) => {
const requestId = createRequestId(req);
if (processedRequests.has(requestId)) {
return res.status(409).send('Duplicate request');
}
processedRequests.add(requestId);
next();
});
// Option 2: Use provider-specific replay protection
// Many providers (Stripe, GitHub) include their own replay protection
// Option 3: Accept the tradeoff
// For many use cases, timestamp validation is sufficientSecurity vs Scalability Tradeoffs
| Approach | Replay Protection | Scalability | Complexity | |----------|------------------|-------------|------------| | Stateless (LeakGuard) | Good | Excellent | Low | | Redis/DB tracking | Perfect | Good | Medium | | Provider built-in | Perfect | Excellent | Low |
Security Best Practices
1. Always Use Raw Body for HMAC
// ✅ Correct - Raw body preserved
app.use('/webhooks', rawBody());
app.use('/webhooks', webhookVerify({ ... }));
// ❌ Wrong - Body already parsed
app.use(express.json());
app.use('/webhooks', webhookVerify({ ... }));2. Use HTTPS Only
// ✅ Force HTTPS in production
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
return res.redirect(`https://${req.header('host')}${req.url}`);
}
next();
});3. Implement Rate Limiting
import rateLimit from 'express-rate-limit';
// ✅ Rate limit webhook endpoints
const webhookLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/webhooks', webhookLimiter);4. Use Different Secrets Per Provider
// ✅ Separate secrets for each provider
const strategies = [
createStripeStrategy(process.env.STRIPE_WEBHOOK_SECRET),
createGitHubStrategy(process.env.GITHUB_WEBHOOK_SECRET),
// Don't reuse secrets across providers
];5. Enable Timestamp Validation
// ✅ Enable timestamp validation for time-sensitive webhooks
const config = {
strategies: [strategy],
timestampValidation: {
timestampHeader: 'x-timestamp',
tolerance: 300 // 5 minutes
}
};Performance
Benchmarks
- HMAC (small payload): ~50,000 ops/sec
- HMAC (large payload): ~5,000 ops/sec
- Token verification: ~100,000+ ops/sec
- IP allowlist: ~150,000+ ops/sec
- Multiple strategies: ~20,000 ops/sec
Optimization Tips
- Use specific strategies: Avoid unnecessary verification steps
- Order strategies by speed: Fast strategies first when using
requireAll: false - Enable replay protection selectively: Only for time-sensitive webhooks
- Use IP allowlisting when possible: Fastest verification method
// ✅ Optimized configuration
const config = {
strategies: [
// Fast strategies first
new IPWhitelistStrategy({ allowedIPs: ['trusted-range'] }),
new TokenStrategy({ token: 'backup-token' }),
// Slower HMAC verification last
new HMACStrategy({ secret: 'hmac-secret' })
],
requireAll: false // Exit early on first success
};Troubleshooting
Common Issues
1. HMAC Verification Fails
// Check these common issues:
// ✅ Ensure raw body is available
console.log('Raw body type:', typeof request.rawBody);
console.log('Raw body length:', request.rawBody?.length);
// ✅ Check signature header format
console.log('Signature header:', request.headers['x-signature']);
// ✅ Verify secret is correct
console.log('Secret (first 4 chars):', secret.substring(0, 4));
// ✅ Check algorithm matches provider
const strategy = new HMACStrategy({
secret: 'correct-secret',
algorithm: 'sha256', // Make sure this matches provider
headerName: 'x-hub-signature-256',
signaturePrefix: 'sha256='
});2. Missing Headers
// ✅ Handle missing headers gracefully
const config = {
strategies: [strategy],
onFailure: (result, request) => {
console.log('Available headers:', Object.keys(request.headers));
console.log('Failure reason:', result.error);
}
};3. Timestamp Issues
// ✅ Debug timestamp validation
const config = {
timestampValidation: {
timestampHeader: 'x-timestamp',
tolerance: 600, // Increase tolerance temporarily
timestampFormat: 'unix'
},
onFailure: (result) => {
console.log('Current time:', Math.floor(Date.now() / 1000));
console.log('Request timestamp:', /* extract from headers */);
}
};Error Handling
Custom Error Responses
// Express custom error handling
app.use('/webhooks', webhookVerify({
strategies: [strategy],
onFailure: (error, req, res, next) => {
// Log for debugging
console.error('Webhook verification failed:', {
error,
headers: req.headers,
ip: req.ip,
url: req.url
});
// Return generic error (don't leak verification details)
res.status(401).json({
error: 'Unauthorized',
timestamp: new Date().toISOString()
});
}
}));
// NestJS exception handling
@UseGuards(new WebhookVerificationGuard(config))
@Post('webhook')
async handleWebhook(@Req() req: Request) {
try {
// Process webhook
return { success: true };
} catch (error) {
// Webhook was verified, but processing failed
throw new InternalServerErrorException('Webhook processing failed');
}
}API Reference
Core Types
interface WebhookRequest {
headers: Record<string, string | string[] | undefined>;
body: Buffer | string;
rawBody?: Buffer;
ip?: string;
}
interface VerificationResult {
verified: boolean;
error?: string;
metadata?: Record<string, unknown>;
}
interface WebhookConfig {
strategies: VerificationStrategy[];
requireAll?: boolean;
timestampValidation?: TimestampValidationOptions;
onFailure?: (result: VerificationResult, request: WebhookRequest) => void;
onSuccess?: (request: WebhookRequest) => void;
}Strategy Classes
HMACStrategy(options: HMACOptions)TokenStrategy(options: TokenOptions)IPWhitelistStrategy(options: IPWhitelistOptions)CustomStrategy(verifyFn: CustomVerificationFunction)
Provider Functions
createStripeStrategy(secret: string, tolerance?: number)createGitHubStrategy(secret: string)createZapierStrategy(token: string)createWebflowStrategy(secret: string)createSlackStrategy(secret: string, tolerance?: number)createDiscordStrategy()createTwilioStrategy()
Framework Adapters
expressWebhookVerify(config: ExpressWebhookOptions)expressRawBody()fastifyWebhookVerifyWebhookVerificationGuard(NestJS)WebhookVerificationMiddleware(NestJS)
Examples
Multi-Provider Webhook Handler
import express from 'express';
import {
createStripeStrategy,
createGitHubStrategy,
createZapierStrategy,
expressWebhookVerify,
expressRawBody
} from '@leakguard/webhook';
const app = express();
// Global raw body middleware
app.use('/webhooks', expressRawBody());
// Stripe webhooks
app.use('/webhooks/stripe', expressWebhookVerify({
strategies: [createStripeStrategy(process.env.STRIPE_WEBHOOK_SECRET!)]
}));
app.post('/webhooks/stripe', (req, res) => {
const event = req.body;
console.log('Stripe event:', event.type);
switch (event.type) {
case 'payment_intent.succeeded':
// Handle payment success
break;
case 'customer.subscription.deleted':
// Handle subscription cancellation
break;
}
res.sendStatus(200);
});
// GitHub webhooks
app.use('/webhooks/github', expressWebhookVerify({
strategies: [createGitHubStrategy(process.env.GITHUB_WEBHOOK_SECRET!)]
}));
app.post('/webhooks/github', (req, res) => {
const event = req.headers['x-github-event'];
const payload = req.body;
console.log('GitHub event:', event);
if (event === 'push') {
// Handle repository push
}
res.sendStatus(200);
});
// Zapier webhooks (token-based)
app.use('/webhooks/zapier', expressWebhookVerify({
strategies: [createZapierStrategy(process.env.ZAPIER_TOKEN!)]
}));
app.post('/webhooks/zapier', (req, res) => {
console.log('Zapier payload:', req.body);
res.sendStatus(200);
});High-Security Configuration
import {
HMACStrategy,
TokenStrategy,
IPWhitelistStrategy,
expressWebhookVerify
} from '@leakguard/webhook';
// Multi-layer verification
const highSecurityConfig = {
strategies: [
// Layer 1: IP allowlist
new IPWhitelistStrategy({
allowedIPs: ['trusted-cidr-range/24'],
trustProxy: true
}),
// Layer 2: Token verification
new TokenStrategy({
token: process.env.BACKUP_TOKEN!,
headerName: 'x-api-key'
}),
// Layer 3: HMAC signature
new HMACStrategy({
secret: process.env.HMAC_SECRET!,
algorithm: 'sha256',
headerName: 'x-signature-256'
})
],
// Require ALL strategies to pass
requireAll: true,
// Strict replay protection
replayProtection: {
timestampHeader: 'x-timestamp',
tolerance: 60, // 1 minute window
timestampFormat: 'unix'
},
onFailure: (result, request) => {
// Log security events
console.error('Security violation:', {
error: result.error,
ip: request.ip,
timestamp: new Date().toISOString(),
headers: request.headers
});
}
};
app.use('/webhooks/critical', expressWebhookVerify(highSecurityConfig));License
MIT - see LICENSE for details.
