easyutils.io / Blog › Webhook Signatures, Explained Properly

Webhook Signatures, Explained Properly

Published: 2026-04-19 · 8 min read

If you're accepting webhooks from any third party — Stripe, GitHub, payment processors, Slack — you're effectively letting them send POST requests to your servers. Without signatures, anyone on the internet who knows your endpoint URL can send those requests. Signatures fix this. Here's how they work and where teams trip.

🔐
Verify a webhook signatureTest HMAC-SHA256 signatures for Stripe, GitHub, and custom schemes.
Open tool →

The problem signatures solve

Without signatures, your webhook endpoint trusts whoever sends a request. An attacker who learns the URL can:

  • Send fake "payment succeeded" events to upgrade their account.
  • Send fake user creation, password reset, or membership events.
  • Flood your endpoint with garbage to break downstream processing.

Authentication via shared API key in a header partly helps — but the key has to be stored, rotated, and exposed in every request. Signatures use the same shared secret but never transmit it; instead, the sender proves it knows the secret by signing each request body.

How HMAC verification works

HMAC (Hash-based Message Authentication Code) is the standard. The sender and receiver share a secret. For each request, the sender:

  1. Takes the request body (often plus a timestamp).
  2. Computes HMAC-SHA256(body, secret).
  3. Sends the result as a header (often X-Signature or similar).

The receiver does the same computation and compares. If the computed value matches what was sent, the request is authentic and unmodified.

The pitfalls

1. Verifying parsed JSON instead of raw bytes

Most frameworks helpfully parse the JSON body before your handler sees it. Re-serializing the parsed object and signing that doesn't produce the same bytes — key order, whitespace, and numeric formatting differ. Always verify against the raw request bytes before any parsing.

2. String comparison instead of timing-safe comparison

Comparing signatures with == leaks timing information that lets an attacker brute-force one byte at a time. Use a constant-time comparison function: hmac.compare_digest in Python, crypto.timingSafeEqual in Node, similar primitives in every language.

3. No timestamp / replay protection

An attacker who captures a single signed request can replay it indefinitely. Stripe, Slack, and others include a timestamp header (often inside the signed payload). The receiver should reject requests with timestamps older than 5 minutes. Store and check seen IDs for sensitive operations.

4. Logging the signature or body

Logs frequently leak. Avoid logging signatures (they're effectively bearer tokens for the request) and full request bodies. Log enough to debug; redact sensitive fields.

5. Using the wrong scheme

Different providers use slightly different formats. Stripe concatenates timestamp + "." + body. GitHub uses just the body with a specific HMAC encoding. Slack uses "v0:" + timestamp + ":" + body. Don't assume — read the docs for the specific provider.

6. Falling back to no verification "for testing"

A common pattern: skip verification when NODE_ENV !== 'production', or accept a special unsigned header in test mode. This often ships to production. Better: use a test-mode secret that you can rotate independently, and verify in all environments.

Secret management

The signing secret is the equivalent of a master password for your webhook endpoint. Treat it accordingly:

  • Never commit it to source. Use environment variables or a secret manager.
  • Rotate periodically. Most providers support multiple active secrets to allow zero-downtime rotation.
  • Use a different secret per environment.
  • If you suspect compromise, rotate immediately and audit recent events.

Verification pseudocode

function verifyWebhook(rawBody, header, secret):
    parts = parse(header)              # e.g. "t=1234567890,v1=abc..."
    timestamp = parts.t
    signature = parts.v1

    if abs(now() - timestamp) > 300:
        reject "stale"

    signed = timestamp + "." + rawBody
    expected = hmacSha256(secret, signed)

    if not timingSafeEqual(signature, expected):
        reject "invalid signature"

    if hasSeenId(parts.id):
        reject "replay"
    markSeen(parts.id)

    accept

Quick checklist

  1. Verify before parsing.
  2. Use timing-safe comparison.
  3. Reject stale timestamps.
  4. Track event IDs for replay protection on sensitive operations.
  5. Don't log the signature or full body.
  6. One secret per environment; rotate on schedule.
  7. Read the provider's spec — don't assume the scheme.
🔐
Verify a webhook signatureTest HMAC-SHA256 signatures for Stripe, GitHub, and custom schemes.
Open tool →
easyutils.io / Blog › Webhook İmzaları, Doğru Açıklanmış

Webhook İmzaları, Doğru Açıklanmış

Yayın tarihi: 2026-04-19 · 8 dk okuma

Üçüncü taraflardan webhook kabul ediyorsanız — Stripe, GitHub, ödeme sağlayıcıları, Slack — onların sunucularınıza POST göndermesine izin veriyorsunuz demektir. İmza olmadan, endpoint URL'inizi bilen internetteki herkes bu istekleri gönderebilir. İmzalar bunu çözer. Nasıl çalışırlar, ekipler nerede takılır:

🔐
Webhook imzasını doğrulaStripe, GitHub ve özel şemalar için HMAC-SHA256 imzalarını test et.
Aracı aç →

İmzaların çözdüğü sorun

İmza olmadan webhook endpoint'iniz isteği gönderen kim olursa olsun güvenir. URL'i öğrenen saldırgan:

  • Hesabını yükseltmek için sahte "ödeme başarılı" eventleri gönderebilir.
  • Sahte kullanıcı oluşturma, şifre sıfırlama veya üyelik eventleri gönderebilir.
  • Endpoint'inizi çöple doldurup downstream işlemi kırabilir.

Header'da paylaşılan API anahtarı bir miktar yardım eder — ama anahtar saklanmalı, rotate edilmeli ve her istekte ifşa edilir. İmzalar aynı paylaşılan secret'ı kullanır ama hiç iletmez; gönderici her istek body'sini imzalayarak secret'ı bildiğini kanıtlar.

HMAC doğrulama nasıl çalışır?

HMAC (Hash-based Message Authentication Code) standarttır. Gönderici ve alıcı bir secret paylaşır. Her istek için gönderici:

  1. Request body'sini alır (genellikle artı bir timestamp).
  2. HMAC-SHA256(body, secret) hesaplar.
  3. Sonucu bir header olarak gönderir (genellikle X-Signature vb.).

Alıcı aynı hesabı yapar ve karşılaştırır. Hesaplanan değer gönderilenle eşleşirse istek otantik ve değiştirilmemiştir.

Tuzaklar

1. Raw byte yerine parse edilmiş JSON'u doğrulamak

Çoğu framework, handler'ınız görmeden JSON body'yi parse eder. Parse edilmiş objeyi yeniden serialize edip onu imzalamak aynı byte'ları üretmez — anahtar sırası, boşluk ve sayısal format farklıdır. Her zaman parse'tan önce raw request byte'larına karşı doğrulayın.

2. Timing-safe karşılaştırma yerine string karşılaştırma

İmzaları == ile karşılaştırmak, saldırganın byte byte brute-force yapmasına izin veren zamanlama sızıntısı yaratır. Sabit zamanlı karşılaştırma kullanın: Python'da hmac.compare_digest, Node'da crypto.timingSafeEqual; her dilde benzer primitif var.

3. Timestamp / replay koruması yok

Tek bir imzalı isteği yakalayan saldırgan, onu süresiz tekrar oynatabilir. Stripe, Slack ve diğerleri timestamp header'ı içerir (sıklıkla imzalanan payload'ın içinde). Alıcı 5 dakikadan eski timestamp'leri reddetmelidir. Hassas işlemler için görülen ID'leri saklayıp kontrol edin.

4. İmzayı veya body'yi loglamak

Loglar sık sık sızar. İmzaları (istek için fiilen bearer token) ve tam request body'yi loglamayın. Debug için yeterince loglayın; hassas alanları maskeleyin.

5. Yanlış şema kullanmak

Farklı sağlayıcılar biraz farklı formatlar kullanır. Stripe timestamp + "." + body birleştirir. GitHub sadece body'yi belirli bir HMAC kodlamasıyla kullanır. Slack "v0:" + timestamp + ":" + body kullanır. Varsayım yapmayın — sağlayıcının dokümanlarını okuyun.

6. "Test için" doğrulamayı atlamak

Yaygın desen: NODE_ENV !== 'production' iken doğrulamayı atlamak veya test modunda özel imzasız bir header kabul etmek. Bu sıklıkla üretime sızar. Daha iyisi: bağımsız rotate edilebilen bir test-mode secret kullanın ve tüm ortamlarda doğrulayın.

Secret yönetimi

İmzalama secret'ı webhook endpoint'iniz için master şifre eşdeğeridir. Buna göre davranın:

  • Asla kaynağa commit etmeyin. Ortam değişkenleri veya secret manager kullanın.
  • Periyodik rotate edin. Çoğu sağlayıcı kesintisiz rotasyon için birden çok aktif secret'ı destekler.
  • Ortam başına farklı secret.
  • Tehlikeden şüpheniz varsa hemen rotate edin, son eventleri denetleyin.

Doğrulama pseudo-kodu

function verifyWebhook(rawBody, header, secret):
    parts = parse(header)              # örn. "t=1234567890,v1=abc..."
    timestamp = parts.t
    signature = parts.v1

    if abs(now() - timestamp) > 300:
        reject "stale"

    signed = timestamp + "." + rawBody
    expected = hmacSha256(secret, signed)

    if not timingSafeEqual(signature, expected):
        reject "invalid signature"

    if hasSeenId(parts.id):
        reject "replay"
    markSeen(parts.id)

    accept

Hızlı liste

  1. Parse'tan önce doğrula.
  2. Timing-safe karşılaştırma kullan.
  3. Eski timestamp'leri reddet.
  4. Hassas işlemlerde event ID takibi yap.
  5. İmzayı veya tam body'yi loglama.
  6. Ortam başına bir secret; planlı rotate.
  7. Sağlayıcının spec'ini oku — şemayı tahmin etme.
🔐
Webhook imzasını doğrulaStripe, GitHub ve özel şemalar için HMAC-SHA256 imzalarını test et.
Aracı aç →