Skip to Content

The Mandrill webhook now checks its signature, without rejecting on it yet

Shipped 2026-09-10

/api/mandrill-webhook sits outside tokenValidate. That is correct — Mandrill cannot present our JWT — but nothing ever replaced the check, so anyone who knows the URL can post a batch of events and have them written into mandrill_logs with a mandrill_id, status and member_id of their choosing. Those rows are not inert: they feed the admin logs page support uses to answer “did this member get their email”, and the daily health check that counts them to decide whether email delivery is working at all. A forged batch can mask a real outage or invent one.

Mandrill’s own answer to this is a signature on every call. X-Mandrill-Signature is an HMAC-SHA1, keyed with the webhook’s key, taken over the registered URL followed by each POST parameter sorted by key. Only Mandrill and we hold the key, so a valid signature proves both where the call came from and that nobody edited it on the way.

Why this ships without enforcing anything

The digest is computed over the URL exactly as registered in Mandrill’s dashboard. If our copy differs by a single character — http for https, a trailing slash, a host the proxy rewrites — every signature we compute is wrong and every call would be rejected. No local test can tell us what was typed into that dashboard, so switching enforcement on from a standing start is a guess.

The previous release logged only whether the header arrived, and that question is now settled: 6,291 calls over three days, every one carrying it, so it survives the DigitalOcean proxy intact. What that could not show is whether our key and our URL actually reproduce the digest. So this release computes the signature and compares it, and does nothing with the answer. A match logs a heartbeat; a mismatch logs at error with the URL we signed against, which is the field that names a trailing-slash bug outright.

Enforcement lives behind MANDRILL_WEBHOOK_ENFORCE_SIGNATURE and is off. With it unset, every request is processed exactly as before, whatever the verdict.

Two details worth keeping in mind:

  • The check runs in the route, not the service. The route answers 200 before processing, because Mandrill retries anything that is not a prompt 2xx. A rejection decided after that acknowledgement is not a rejection at all — the batch has already been accepted — so the verdict has to be reached before the response goes out.
  • A missing key reads as unconfigured, never as invalid. If those two collapsed into one value, a key that went astray in Infisical would look exactly like a forged request, and enforcement would reject every genuine call in the same breath. The distinction is pinned by a test.

Before it does anything

MANDRILL_WEBHOOK_URL and MANDRILL_WEBHOOK_KEY have to reach the running API. Copy the URL verbatim from Mandrill → Webhooks rather than retyping it; that is the precise failure this staged rollout exists to catch. Until both are present the verdict is unconfigured on every call.

Once they are in place, watch for mandrill_webhook.signature_invalid. Silence across a day of traffic — roughly 2,000 events — means the URL and key are right and the flag can be turned on in a follow-up.

Last updated on