Skip to main content

Free 30-min security demo  — We'll scan your real code and show live findings, no commitment Book Now

Offensive360
Academy Webhook Security
Intermediate · 20 min

Webhook Security

Learn how missing webhook signature validation and SSRF via webhook URLs compromise server security.

1 SSRF via Webhook URLs and Missing Signature Validation

Webhooks receive POST requests from external services. Two critical vulnerabilities arise: attackers can register malicious webhook URLs (SSRF), and webhook data can be forged without signature validation.

SSRF via webhook URL:

POST /api/webhooks
{ "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/" }
# Server fetches the AWS metadata endpoint, returning cloud credentials!

The attacker registers a webhook pointing to an internal service. When the webhook fires, the server makes a request to the internal URL — leaking internal data or enabling further attacks.

Missing signature validation:

// Vulnerable: trusts all incoming webhook data
app.post("/webhook/payment", async (req, res) => {
  const { orderId, status } = req.body;
  if (status === "completed") {
    await markOrderPaid(orderId);  // Attacker sends fake "completed" event!
  }
  res.sendStatus(200);
});

2 HMAC Signature Verification and URL Allowlisting

Verify webhook authenticity with HMAC signatures and restrict webhook URLs to allowed destinations.

HMAC signature verification (Stripe-style):

const crypto = require("crypto");

app.post("/webhook/payment",
  express.raw({ type: "application/json" }),  // Keep raw body for sig check
  async (req, res) => {
    const sig = req.headers["x-webhook-signature"];
    const expectedSig = crypto
      .createHmac("sha256", process.env.WEBHOOK_SECRET)
      .update(req.body)
      .digest("hex");

    if (!crypto.timingSafeEqual(
      Buffer.from(sig, "hex"),
      Buffer.from(expectedSig, "hex")
    )) {
      return res.status(400).send("Invalid signature");
    }
    // Process verified webhook...
  }
);

URL allowlisting for outbound webhooks:

const ALLOWED_URL_REGEX = /^https:\/\/[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+(\/|$)/;

function validateWebhookUrl(url) {
  const parsed = new URL(url);
  // Block private IP ranges and localhost
  if (/^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|127\.|169\.254\.)/.test(parsed.hostname)) {
    throw new Error("Private IP addresses not allowed for webhooks");
  }
  if (!["http:", "https:"].includes(parsed.protocol)) {
    throw new Error("Only HTTP/S webhook URLs allowed");
  }
}

Knowledge Check

0/3 correct
Q1

How does SSRF via webhook URLs threaten cloud-hosted applications?

Q2

Why must the raw request body be used for webhook HMAC verification?

Q3

What should happen if a webhook request fails HMAC signature verification?

Code Exercise

Verify Webhook Signature

The webhook endpoint processes all incoming requests without signature validation. Add HMAC-SHA256 signature verification using the X-Webhook-Signature header.

javascript