Optional inbound webhook HMAC signature verification #67

Closed
opened 2026-08-07 13:11:19 +02:00 by clawbot · 2 comments
Collaborator

Post-1.0 hardening (surfaced during the 1.0 review, see #33 — not a 1.0 blocker).

The receiver (internal/handlers/webhook.go) does not verify sender authenticity. Many webhook senders sign payloads (e.g. GitHub-style HMAC-SHA256 over the body with a shared secret). Supporting optional verification lets operators reject spoofed events.

Definition of done:

  • an optional per-entrypoint shared secret and signature scheme (header name + algorithm)
  • when a secret is configured, requests with a missing or invalid signature are rejected with HTTP 401
  • when no secret is configured, behaviour is unchanged
  • a test covers both valid and invalid signatures
Post-1.0 hardening (surfaced during the 1.0 review, see #33 — not a 1.0 blocker). The receiver (`internal/handlers/webhook.go`) does not verify sender authenticity. Many webhook senders sign payloads (e.g. GitHub-style HMAC-SHA256 over the body with a shared secret). Supporting optional verification lets operators reject spoofed events. Definition of done: - an optional per-entrypoint shared secret and signature scheme (header name + algorithm) - when a secret is configured, requests with a missing or invalid signature are rejected with HTTP 401 - when no secret is configured, behaviour is unchanged - a test covers both valid and invalid signatures
clawbot added this to the 1.0.0 milestone 2026-08-20 05:48:59 +02:00
Author
Collaborator

Reclassified to the 1.0.0 milestone. The body above still says "not a 1.0 blocker" — that is superseded.

The 2026-08-20 deployability audit established there is no inbound authentication of ANY kind: internal/server/routes.go:219-224 mounts /webhook/{uuid} behind ReceiverRateLimit() alone, the Entrypoint model has no secret column, and a grep for hmac|signature|x-hub-signature|stripe-signature across internal/ returns zero non-comment hits. The only thing protecting an entrypoint is the secrecy of a v4 UUID in a URL path.

Two things make that worse than "unauthenticated but obscure". Inbound headers are forwarded almost verbatim to the target (internal/delivery/target_http.go:443-491 strips only hop-by-hop plus Proxy-Authorization), so anyone who learns the URL also chooses the headers the downstream service receives. And "durably stored" is the product's core claim, which is worth much less when provenance is unknown.

Scope unchanged from the definition of done above, with one addition: the per-entrypoint secret must be settable and rotatable from the UI, and support at least the GitHub X-Hub-Signature-256 and GitLab X-Gitlab-Token shapes, since those are the senders that make the feature necessary. Rejection happens before persistence.

Reclassified to the `1.0.0` milestone. The body above still says "not a 1.0 blocker" — that is superseded. The 2026-08-20 deployability audit established there is no inbound authentication of ANY kind: `internal/server/routes.go:219-224` mounts `/webhook/{uuid}` behind `ReceiverRateLimit()` alone, the `Entrypoint` model has no secret column, and a grep for `hmac|signature|x-hub-signature|stripe-signature` across `internal/` returns zero non-comment hits. The only thing protecting an entrypoint is the secrecy of a v4 UUID in a URL path. Two things make that worse than "unauthenticated but obscure". Inbound headers are forwarded almost verbatim to the target (`internal/delivery/target_http.go:443-491` strips only hop-by-hop plus `Proxy-Authorization`), so anyone who learns the URL also chooses the headers the downstream service receives. And "durably stored" is the product's core claim, which is worth much less when provenance is unknown. Scope unchanged from the definition of done above, with one addition: the per-entrypoint secret must be settable and rotatable from the UI, and support at least the GitHub `X-Hub-Signature-256` and GitLab `X-Gitlab-Token` shapes, since those are the senders that make the feature necessary. Rejection happens before persistence.
Author
Collaborator

Reversed and removed by #279, landed on next.

Owner's decision, 2026-08-24: "remove all the inbound request signing crap. the uuid url is the authentication secret." The entrypoint URL is a capability — possession of the UUID authorises submission, and that is the whole model.

The implementation this issue asked for is gone: internal/signature deleted, the model fields and forms removed, the receiver verifying nothing inbound. Independent review confirmed no neighbouring receiver guard was weakened — the 1 MB cap, per-IP rate limiting, the 404/410 lookup and the cross-origin redirect header protection are all intact and were re-proven on a live instance.

Closing.

Reversed and removed by https://git.eeqj.de/sneak/webhooker/issues/279, landed on `next`. Owner's decision, 2026-08-24: "remove all the inbound request signing crap. the uuid url is the authentication secret." The entrypoint URL is a capability — possession of the UUID authorises submission, and that is the whole model. The implementation this issue asked for is gone: `internal/signature` deleted, the model fields and forms removed, the receiver verifying nothing inbound. Independent review confirmed no neighbouring receiver guard was weakened — the 1 MB cap, per-IP rate limiting, the 404/410 lookup and the cross-origin redirect header protection are all intact and were re-proven on a live instance. Closing.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/webhooker#67