How to Secure Your Webhook Endpoints

A public HTTPS endpoint that accepts POST requests from the internet needs multiple layers of security. Learn the complete security checklist for production JsonHook webhook endpoints.

Table of Contents
  1. Overview
  2. Prerequisites
  3. Step-by-Step Instructions
  4. Code Example
  5. Common Pitfalls

Overview

Webhook endpoints are publicly reachable HTTP endpoints — which means attackers can probe them. A poorly secured webhook endpoint can be exploited to:

  • Inject forged email payloads that trigger unintended actions in your application
  • Execute denial-of-service attacks by flooding the endpoint with requests
  • Exploit processing bugs by crafting malicious payload content
  • Leak information through error responses that reveal application internals

A complete webhook security posture uses multiple layers:

  1. HMAC-SHA256 signature verification — authenticate that requests come from JsonHook
  2. TLS enforcement — encrypt traffic in transit
  3. Rate limiting — prevent request flooding
  4. Input validation — reject malformed payloads early
  5. Secrets management — protect webhook secrets from leakage
  6. Minimal response bodies — do not leak internal details in error responses

Prerequisites

Security prerequisites:

  • A valid TLS certificate from a recognized CA for your webhook endpoint domain
  • A rate limiting layer (application-level middleware, API gateway, or reverse proxy like nginx)
  • A secrets management solution (AWS Secrets Manager, HashiCorp Vault, environment variables with restricted access)
  • Input validation library for your application framework

Secure Your Email Webhook Pipeline

HMAC-SHA256 on every delivery. Your endpoint, locked down tight.

Get Free API Key

Step-by-Step Instructions

Implement each security layer:

  1. Enforce HMAC-SHA256 verification as the first middleware step — reject before any processing.
  2. Enforce HTTPS only:
    app.use((req, res, next) => {
      if (req.protocol !== "https") return res.status(301).redirect(`https://${req.hostname}${req.url}`);
      next();
    });
  3. Add rate limiting using express-rate-limit or equivalent:
    import rateLimit from "express-rate-limit";
    const webhookLimiter = rateLimit({ windowMs: 60_000, max: 200 });
    app.use("/webhooks", webhookLimiter);
  4. Validate payload structure after signature verification — check required fields are present and have expected types.
  5. Return minimal error responses — never include stack traces or internal details in 4xx/5xx responses from webhook endpoints.
  6. Rotate secrets regularly — use the JsonHook secret rotation API and update your environment variables promptly.

Code Example

Complete secure webhook middleware stack:

import express from "express";
import crypto from "crypto";
import rateLimit from "express-rate-limit";

const app = express();

// 1. Rate limiting — before signature verification to limit cost
const webhookLimiter = rateLimit({
  windowMs: 60_000,
  max: 500,
  standardHeaders: true,
  legacyHeaders: false,
  message: "Too many requests",
});

// 2. Raw body parsing for HMAC
app.use("/webhooks", express.raw({ type: "application/json", limit: "10mb" }));
app.use("/webhooks", webhookLimiter);

// 3. Signature verification middleware
function requireJsonHookSignature(req: express.Request, res: express.Response, next: express.NextFunction) {
  const sig = req.headers["x-jsonhook-signature"];
  if (!sig || typeof sig !== "string") return res.sendStatus(401);

  const expected = crypto
    .createHmac("sha256", process.env.JSONHOOK_SECRET!)
    .update(req.body)
    .digest("hex");

  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.sendStatus(401); // No detail in response body
  }
  next();
}

// 4. Payload schema validation
function validatePayload(req: express.Request, res: express.Response, next: express.NextFunction) {
  try {
    const payload = JSON.parse(req.body.toString());
    if (!payload.email?.from || !payload.deliveryId) return res.sendStatus(400);
    req.body = payload; // replace buffer with parsed object
    next();
  } catch {
    res.sendStatus(400);
  }
}

app.post("/webhooks/email",
  requireJsonHookSignature,
  validatePayload,
  async (req: any, res) => {
    // Safe to process req.body as a validated, authenticated payload
    res.sendStatus(200);
  }
);

Common Pitfalls

Security mistakes to avoid:

  • Secrets in environment variables accessible to all processes. Use per-service secrets — the JsonHook webhook secret should only be accessible to the process that runs the webhook handler, not to every service in your application.
  • No rate limiting on unauthenticated requests. Rate limiting should apply before signature verification so attackers cannot exhaust your server resources by sending many unsigned requests that get processed before rejection.
  • Verbose error responses. Returning {"error": "HMAC mismatch: expected abc123 got xyz789"} leaks your expected hash. Return only status codes for security-related errors.
  • Not renewing TLS certificates. An expired certificate causes all webhook deliveries to fail and triggers a JsonHook alert. Automate certificate renewal (Let's Encrypt + certbot) or use a managed TLS service.
  • Assuming internal-only endpoints are safe. Even if your webhook endpoint is behind a VPN or firewall, implement HMAC verification. Defense in depth means you do not rely solely on network-level controls.

Frequently Asked Questions

Is HMAC verification sufficient to secure my webhook endpoint?

HMAC verification ensures that only JsonHook can send valid requests to your endpoint — it is the primary security control. You should also add rate limiting to prevent DoS, enforce HTTPS, validate payload structure, and manage secrets securely. Together, these layers provide defense in depth for a production webhook endpoint.

Should I allowlist JsonHook's IP addresses?

IP allowlisting is an optional additional control. JsonHook publishes its outbound IP ranges for customers who want to restrict by IP. However, this adds operational overhead (IP ranges can change) and is secondary to HMAC verification. Rely on HMAC as your primary authentication mechanism; add IP allowlisting as an extra layer if your security policy requires it.

How do I handle secrets securely in a cloud environment?

Store the JsonHook webhook secret in a secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, Doppler) and inject it as an environment variable at runtime. Never commit secrets to source code or expose them in CI/CD logs. Rotate secrets on a regular schedule and immediately after any suspected compromise.

What should I log for webhook security auditing?

Log: timestamp, delivery ID, source IP, response status code, and response time for every webhook request. Log signature verification failures with the source IP (but not the received or expected signature values). Retain security logs separately from application logs with a longer retention period for audit purposes.