> ## Content Index
> Fetch the complete content index at: https://debugly.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# The Webhook Signature Nobody Verified
- URL: https://debugly.dev/webhook-signature-never-verified/
- Published: 2026-10-09T17:16:00.000Z
- Updated: 2026-10-17T05:55:40.000Z
- Description: The endpoint was public and trusted the body, so a crafted request marked an order as paid, and the fulfilment ran, and the signature header had been…
- Author: Rohit Bhadani
- Tags: Error Autopsy, Security, Webhooks

The shipped behaved. The loss behaved. The chain between them is where working as designed stopped meaning working.

The unverified webhook has a single weak property, and here it is: the signature header is present on every real request, so nothing looks absent, and the verification is the code that was never written rather than the code that broke.

The case was a payment webhook on a public endpoint; the mechanics generalise to any inbound integration whose authenticity depends on a signature the receiver does not check.

## Why the endpoint trusted the body

The endpoint was the POST route, and the route was the public, and the public was the necessary, because the provider calls it from their infrastructure, and the infrastructure is the unknown IPs, and the unknown is the IP allowlist's difficulty, and the difficulty is the signature's purpose, and the purpose is the authenticity, and the authenticity is the verification, and the verification was the absent.

The body was the parsed JSON, and the JSON was the order identifier and the status, and the two were the trusted, and the trusted was the assumption, and the assumption was the sender's identity, and the identity was the unverified, and the unverified was the anyone, and the anyone was the attacker, and the attacker was the crafted request, and the request was the shipped order.

The signature was the header, and the header was the present, and the present was the invisible, because the code never read it, and the never read was the not a bug but an absence, and the absence is the harder review catch, because there is no wrong line to spot, only the missing one.

## Why the review did not catch it

The review saw the handler, and the handler was the parse and the update, and the two were the correct, and the correct was the pass, because the code did what it said, and the said was the not the whole job, and the whole job was the authenticate then the parse, and the authenticate was the absent, and the absent was the miss.

The review's question should have been how does this endpoint know the request is genuine, and the question is the signature, and the signature is the check, and the check is the constant time comparison, and the comparison is the fix, and the fix is the ten lines, and the lines were the absent, and the absent was the incident.

This is the same delegation error as [the JWT verifier that accepted the none algorithm](https://debugly.dev/jwt-accepted-none-algorithm/), and the common root is the property, not the platform.

## The fixes

**Verified the signature before parsing the body.** The header is the computed HMAC, and the computed is the comparison, and the comparison is the constant time, and the constant time is the timing attack's prevention, and the prevention is the fix, and the fix is the order, because the parse before the verify is the trust before the check.

**Rejected the request on the mismatch with a 401.** The mismatch is the rejection, and the rejection is the loud, and the loud is the log, and the log is the attacker's probe, and the probe is the detection, and the detection is the fix, and the fix is the alert on the repeated failure.

**Used the raw body for the signature computation.** The raw is the exact bytes, and the exact is the HMAC's input, and the input is the parse's difference, and the difference is the mismatch, and the mismatch is the false rejection, and the rejection is the bug, and the bug is the reason the raw body matters, because the re-serialised JSON differs in whitespace and key order.

**Checked the timestamp to reject the replay.** The timestamp is the window, and the window is the replay's limit, and the limit is the fix, and the fix is the tolerance, and the tolerance is the minutes, and the minutes is the discipline, because the captured valid request is the replayable one without the window.

**Logged the verification failures with the source address.** The logged is the pattern, and the pattern is the attack, and the attack is the detection, and the detection is the fix, and the fix is the monitoring, and the monitoring is the discipline, because the unverified endpoint's abuse is invisible without the failure log.

## What I now do

A public webhook endpoint must verify the provider's signature before parsing the body, using a constant-time comparison against the raw request bytes, and must reject the mismatch and check the timestamp against a replay window. Log the failures, because the verification is the only thing distinguishing the provider from anybody else with the URL.

A crafted POST marked an unpaid order as paid and the warehouse shipped it, because the handler parsed the body and acted on it without ever reading the signature header the provider had been sending all along. The lesson I keep is the property, not the incident.

---

Published on [debugly.dev](https://debugly.dev/) by [Rohit Bhadani](https://debugly.dev/about/) | Founder of [Krova Cloud](https://krova.cloud/?ref=debugly.dev)