Back to all articles
Security

Webhook Security: Protecting Your Integration Layer

Webhooks are the backbone of SaaS integrations — and one of the most commonly misconfigured attack surfaces. Here's how to lock them down.

Amirol AhmadAmirol Ahmad
April 20, 2026
6 min read
Share on X
Webhook Security: Protecting Your Integration Layer

Webhooks power the integrations that modern SaaS products depend on — Stripe payment events, GitHub push hooks, Slack notifications, n8n automation flows. They're also one of the most frequently misconfigured attack surfaces in a SaaS stack.

A poorly secured webhook endpoint can be used to inject fake events, trigger unauthorized actions, or exhaust server resources. This guide covers the complete picture: validating incoming webhooks, securing outbound delivery, and using LiteSOC to monitor your integration layer.

The Threat Model

Webhooks face threats from two directions:

Incoming webhooks (you receive)

  • Forged payloads — an attacker sends fake events to your endpoint
  • Replay attacks — a legitimate captured payload is re-sent to trigger repeated actions
  • SSRF via misconfigured routing — rare but possible in certain proxy configurations

Outgoing webhooks (you send)

  • SSRF attacks — a user registers a webhook URL pointing to an internal service (e.g., http://169.254.169.254/)
  • Data exfiltration — sensitive event data delivered to an attacker-controlled endpoint
  • Delivery exhaustion — a flood of events overwhelms a legitimate downstream service

Most tutorials only cover half of this. Let's cover all of it.

Securing Incoming Webhooks

1. Always Verify Signatures

Every serious webhook provider (Stripe, GitHub, Resend, LiteSOC) issues an HMAC-SHA256 signature with each payload. Never process a webhook payload without verifying this signature.

Here's how Stripe signature verification works in principle — the same pattern applies to most providers:

import crypto from 'crypto';

function verifyWebhookSignature(
  rawBody: string,
  signature: string,
  secret: string,
  toleranceSeconds = 300
): boolean {
  const parts = signature.split(',').reduce<Record<string, string>>((acc, part) => {
    const [key, value] = part.split('=');
    acc[key] = value;
    return acc;
  }, {});

  const timestamp = parts['t'];
  const receivedSig = parts['v1'];

  // Reject stale payloads (replay protection)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > toleranceSeconds) {
    throw new Error('Webhook timestamp too old');
  }

  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expectedSig, 'hex'),
    Buffer.from(receivedSig, 'hex')
  );
}

Two critical details here:

  • Use crypto.timingSafeEqual to prevent timing attacks
  • Always check the timestamp to prevent replay attacks

2. Read the Raw Body

Signature verification must use the raw, unparsed request body. If you run JSON.parse() first and then re-stringify, the byte-for-byte content may differ and the HMAC will fail.

In Next.js App Router:

export async function POST(req: Request) {
  const rawBody = await req.text(); // NOT req.json()
  const signature = req.headers.get('stripe-signature') ?? '';

  const isValid = verifyWebhookSignature(rawBody, signature, process.env.STRIPE_WEBHOOK_SECRET!);
  if (!isValid) {
    return new Response('Invalid signature', { status: 401 });
  }

  const event = JSON.parse(rawBody);
  // process event...
}

3. Implement Idempotency

Webhook providers retry delivery on failure. Your handler must be idempotent — processing the same event ID twice must produce the same result as processing it once.

Store processed event IDs in Redis with a TTL matching your provider's retry window:

const eventId = event.id; // e.g., "evt_1ABC..."
const key = `webhook:processed:${eventId}`;

const alreadyProcessed = await redis.get(key);
if (alreadyProcessed) {
  return new Response('OK', { status: 200 }); // Ack without reprocessing
}

// Process the event...

await redis.set(key, '1', { ex: 86400 }); // 24-hour TTL

Securing Outgoing Webhooks

1. Validate URLs Before Delivery (SSRF Prevention)

If your product lets users register their own webhook URLs, you must validate every URL before making a request to it. An attacker could register http://169.254.169.254/latest/meta-data/ to exfiltrate AWS instance credentials.

A robust isValidWebhookUrl function:

import { URL } from 'url';
import dns from 'dns/promises';

const BLOCKED_RANGES = [
  /^127\./,
  /^10\./,
  /^172\.(1[6-9]|2\d|3[01])\./,
  /^192\.168\./,
  /^169\.254\./,   // Link-local / AWS metadata
  /^::1$/,         // IPv6 loopback
  /^fc00:/,        // IPv6 private
];

export async function isValidWebhookUrl(rawUrl: string): Promise<boolean> {
  let parsed: URL;
  try {
    parsed = new URL(rawUrl);
  } catch {
    return false;
  }

  // Only allow HTTPS
  if (parsed.protocol !== 'https:') return false;

  // Resolve hostname to IP and check against blocked ranges
  const addresses = await dns.resolve4(parsed.hostname).catch(() => []);
  for (const ip of addresses) {
    if (BLOCKED_RANGES.some((pattern) => pattern.test(ip))) {
      return false;
    }
  }

  return true;
}

2. Sign Your Outgoing Payloads

When LiteSOC delivers alerts to your registered webhook URL, every payload is signed with an HMAC-SHA256 signature in the X-LiteSOC-Signature header. Your endpoint can verify this to confirm the payload came from LiteSOC and hasn't been tampered with in transit.

This is the same pattern you should apply when building webhook delivery in your own product.

3. Enforce Delivery Timeouts

Never wait indefinitely for a webhook recipient. Set a hard timeout (5–10 seconds max) and implement exponential backoff for retries:

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 8000); // 8s timeout

try {
  const res = await fetch(webhookUrl, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Webhook-Signature': signature,
    },
    body: JSON.stringify(payload),
    signal: controller.signal,
  });

  if (!res.ok) {
    // Schedule retry with exponential backoff
  }
} catch (err) {
  // Timeout or network error — schedule retry
} finally {
  clearTimeout(timeoutId);
}

Monitoring Webhooks with LiteSOC

Your webhook infrastructure is a critical integration layer. Any anomaly — failed verifications, unexpected event types, delivery failures — should be a security signal.

LiteSOC's security.* events are designed for exactly this. You can instrument your webhook handler like this:

// On signature verification failure
await litesoc.track({
  event_name: 'security.webhook_signature_invalid',
  metadata: {
    source: 'stripe',
    endpoint: '/api/webhooks/stripe',
    ip_address: req.headers.get('x-forwarded-for'),
  },
});

// On suspicious replay attempt
await litesoc.track({
  event_name: 'security.webhook_replay_attempt',
  metadata: {
    event_id: eventId,
    source: 'stripe',
    age_seconds: staleness,
  },
});

With these events flowing into LiteSOC, you can build alert rules like:

PatternSeverity
3+ signature failures from same IP in 5 minHigh
Replay attempt on already-processed event IDHigh
Webhook URL registration with private IP targetCritical
Delivery failure rate > 20% in 10 minMedium

Checklist

Before going live with any webhook integration, verify:

  • All incoming webhook signatures are verified with timingSafeEqual
  • Raw body (not parsed JSON) is used for signature verification
  • Timestamp tolerance check is in place (≤300 seconds)
  • Idempotency keys are stored with appropriate TTL
  • All user-registered webhook URLs pass SSRF validation
  • Outgoing requests have a hard timeout (≤10s)
  • Failed verifications are logged as security.* events in LiteSOC
  • Retry logic uses exponential backoff with a dead-letter record

Conclusion

Webhooks are not passive receivers, they're privileged execution paths. A forged event can trigger a refund, elevate permissions, or exfiltrate data depending on your business logic. Treating every incoming webhook with the same skepticism you'd apply to any unauthenticated request is the right mental model.

Signature verification, replay prevention, SSRF-safe URL validation, and LiteSOC monitoring together give you a complete defense-in-depth strategy for your integration layer.

Stay Updated

Get the latest security insights and product updates delivered to your inbox. No spam, unsubscribe anytime.