ADR-0027: Webhook Security with HMAC SHA-256
Status
Accepted - 2025-01-26
Context
TVL Platform receives webhooks from Hostaway, Airbnb, VRBO with sensitive booking/property data requiring verification.
Decision
HMAC SHA-256 signature verification for all incoming webhooks.
Rationale
- Industry Standard: Used by Stripe, GitHub, Shopify
- Tamper-Proof: Any payload modification invalidates signature
- Replay Attack Prevention: Combined with timestamp validation
- Simple: Single header verification (X-SignatureorX-Hub-Signature-256)
Alternatives Considered
Alternative 1: API Keys Only
Rejected - No payload integrity verification, vulnerable to MITM attacks
Alternative 2: Mutual TLS (mTLS)
Rejected - Complex certificate management, overkill for webhook verification
Alternative 3: JWT Signatures
Rejected - Heavier than HMAC, requires token parsing overhead
Implementation
// src/webhooks/verifyWebhookSignature.ts
import crypto from 'crypto';
export function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  // Constant-time comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}
// src/routes/webhooks/hostaway.ts
app.post('/webhooks/hostaway', async (req, reply) => {
  const signature = req.headers['x-hostaway-signature'] as string;
  const payload = JSON.stringify(req.body);
  const secret = process.env.HOSTAWAY_WEBHOOK_SECRET!;
  if (!verifyWebhookSignature(payload, signature, secret)) {
    return reply.status(401).send({ error: 'Invalid signature' });
  }
  // Process webhook...
  await processHostawayWebhook(req.body);
  return reply.status(200).send({ received: true });
});
Security Measures
1. Timestamp Validation (5-minute window)
export function isTimestampValid(timestamp: number): boolean {
  const now = Date.now() / 1000;
  const diff = Math.abs(now - timestamp);
  return diff < 300; // 5 minutes
}
2. Request ID Deduplication
const processedWebhooks = new Set<string>();
if (processedWebhooks.has(requestId)) {
  return reply.status(200).send({ received: true }); // Already processed
}
processedWebhooks.add(requestId);
3. IP Allowlist (Optional)
const HOSTAWAY_IPS = ['54.123.45.67', '54.123.45.68'];
if (!HOSTAWAY_IPS.includes(req.ip)) {
  return reply.status(403).send({ error: 'Forbidden' });
}
Consequences
Positive
- ✅ Strong Security: Cryptographic payload verification
- ✅ Replay Protection: Timestamp + request ID validation
- ✅ Partner Compatibility: All partners support HMAC
Negative
- ❌ Secret Management: Requires secure storage (Doppler)
- ❌ Clock Skew: Timestamp validation may fail if clocks drift
Mitigations
- Store secrets in Doppler (SOC 2 certified)
- Use 5-minute window for timestamp validation (tolerates drift)
- Log failed verifications for monitoring
Validation Checklist
- HMAC SHA-256 signature verification enabled
- Timestamp validation (5-minute window)
- Request ID deduplication
- Secrets stored in Doppler
- Failed verification logging