Webhook, internetin kapıyı çalıp içerideki işi yapmasıdır: ödeme sağlayıcısı, sürüm sistemi veya mesajlaşma servisi, sizin bir adresinize HTTP isteği atar. Sorun şudur: o adrese herkes istek atabilir. Güvenlik denetimi olmadan webhook ucu, sunucunuza ücretsiz ve anonim bir komut yürütme kapısı bırakmaktır.

İşin sinsi tarafı, saldırının çoğu zaman kötücül bile olması gerekmemesidir: adresi bilen herkes, yanlışlıkla dahil, işleyicinizi tetikleyebilir. Test sırasında uç noktanızı tarayıcıda açan bir ekip arkadaşınız bile, ödeme onayı sanılan bir kayıt bırakabilir. Doğrulama katmanı, bu tür kazaları kötücül saldırıdan ayıran tek çizgidir.

İmza: Paylaşılan Sır, Doğrulanmış Gövde

Doğru desen, göndericinin gövdeyi paylaşılan bir sır ile HMAC algoritmasıyla imzalamasıdır. Alıcı aynı sırdan aynı imzayı üretir ve eşleşmezse isteği okumaz bile. İmza gövdeye bağlı olduğu için, içerikteki tek bir karakter değişikliği imzayı geçersiz kılar.

const ozet = crypto
  .createHmac('sha256', process.env.WEBHOOK_SIRI)
  .update(govde)
  .digest('hex');

const gecenler = crypto.timingSafeEqual(
  Buffer.from(ozet), Buffer.from(imzaBasligi),
);
if (!gecenler) return new Response('401', { status: 401 });

Karşılaştırmayı timingSafeEqual gibi sabit zamanlı bir fonksiyonla yapmak gerekir; normal eşitlik, imzayı karakter karakter test etmeye çalışan bir saldırgana süre sızdırır.

Bir de sırrın kendisinin yaşam döngüsü vardır: sızdırılan bir anahtarı değiştirebilmek için iki anahtarı destekleyin, yenisiyle imzalayın, eskisiyle doğrulamayı bir süre sürdürün. Anahtar değişiminin zor olduğu bir sistemde, sızma sonrası iyileşme de zordur.

Zaman Damgası: Tekrarlama Saldırısına Karşı

İmza tek başına yeterli değildir: saldırgan, geçerli bir isteği ağdan yakalayıp imzasıyla birlikte tekrar gönderebilir. Savunma, isteğe zaman damgası koymak ve alıcı tarafında bir tolerans tanımaktır. İstek birkaç dakikadan eskiyse reddedilir; böylece yakalanan bir imza, sınırlı bir süre sonra ölü doğar.

const fark = Math.abs(Date.now() - zamanDamgasi);
if (fark > 5 * 60 * 1000) return new Response('401', { status: 401 });

Daha sıkı bir savunma, işlenmiş isteklerin kimliğini kısa süreli bir önbellekte tutmaktır; aynı istek ikinci kez gelirse, imzası geçerli olsa bile atlanır. Bu, ödeme bildirimlerinde mükerrer kaydı önler.

Zaman toleransını belirlerken saat senkronizasyonunu da hesaba katın: göndericiyle alıcının saati birkaç saniye farklı olabilir ve sunucular arası küçük kaymalar normaldir. Beş dakikalık tolerans, saldırı penceresini kapatmaya yeterli ve saat sapmasına dayanıklı olmak arasında pratik bir orta noktadır.

Tekrarlama Sorun Değil, Gerçek

Saldırgan dışında tekrarlayan bir taraf daha vardır: göndericinin kendisi. Ağ kesintisi, zaman aşımı veya sizin yavaş yanıtınız, sağlayıcının aynı olayı iki kez bildirmesine yol açar. Bu yüzden webhook işleyicisi idempotent yazılmalıdır: olay kimliğini veritabanına kaydedin, daha önce işlenmişse aynı işi tekrarlamayın.

Webhook güvenliği üç katmandan oluşur: imza, göndereni doğrular; zaman damgası, eski imzayı öldürür; idempotent işleyici, tekrarları zararsız kılar. Biri eksik olduğunda, diğer ikisi onun deliğini kapatmak için vardır. Üçünü de koyun; açık uçlu bir webhook, faturayı en kötü günde keser.

Son bir pratik öneri: doğrulamadan geçemeyen istekleri de günlüğe kaydedin. Başarısız doğrulamaların deseni, bir sır değişimini kaçırdığınızı ya da sağlayıcının şemasını değiştirdiğini erken gösterir; sessizce reddedilen istek, kendisinden haber vermediği için en tehlikelisidir. Kayıt tutan bir uçta, "fatura gelmedi" şikâyeti geldiğinde iki dakikada cevap bulabilirsiniz: imza mı düşmüştü, zaman damgası mı eskimişti, yoksa olay hiç mi ulaşmamıştı?