Skip to main content

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

  1. Industry Standard: Used by Stripe, GitHub, Shopify
  2. Tamper-Proof: Any payload modification invalidates signature
  3. Replay Attack Prevention: Combined with timestamp validation
  4. Simple: Single header verification (X-Signature or X-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

References