İmza doğrulama
Her webhook teslimi birlikte kullanılan iki başlık içerir:
X-Signature: sha256=9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b
X-Timestamp: 1729536000sha256=’den sonraki hex string,
HMAC-SHA256(secret, timestamp + "." + raw_body)’dir. Nokta gerçek bir
bayttır; timestamp ASCII olarak unix-saniyedir.
Neden doğrulamalısınız
Webhook URL’leri sızar. Proxy loglarında, ekran görüntülerinde, tarayıcı
geçmişinde, partner destek taleplerinde görünürler. Bir imza kontrolü
olmadan, URL’nizi öğrenen herkes sahte bir payment.settled event’i POST
edebilir ve sizi ödenmemiş siparişleri karşılamaya kandırabilir.
Doğrulama, isteğin InfraIO’dan geldiğini kriptografik olarak kanıtlar.
İmzalanmış payload’ın içine timestamp’i dahil etmek size ayrıca replay koruması sağlar: bir teslim yakalayan saldırgan, imza algılanabilir şekilde eskimeden onu daha sonra yeniden gönderemez.
Algoritma
signed_payload = timestamp + "." + raw_body
expected_header = "sha256=" + lowercase_hex( HMAC_SHA256(secret, signed_payload) )
constant_time_compare(expected_header, x_signature_header)Ardından timestamp’in yakın olduğunu kontrol edin (tipik tolerans: ±5 dakika).
Her zaman ham istek gövdesi baytlarını iletin. Framework’ler
genellikle handler’ınız çalışmadan önce JSON’ı ayrıştırır; yeniden
stringleştirilmiş sürüm gönderdiğimizden farklı olabilir (anahtar
sırası, boşluk, sayı biçimlendirmesi) ve HMAC eşleşmez. Next.js App
Router’da JSON.parse’tan önce await req.text() kullanın.
Express’te yalnızca webhook rotasında
express.raw({ type: 'application/json' }) mount edin.
Uygulamalar
Node / TS
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 5 * 60;
export function verifyInfraIo({
body,
signature,
timestamp,
secret,
}: {
body: string; // ham metin — ayrıştırılmış JSON DEĞİL
signature: string; // X-Signature başlığının değeri
timestamp: string; // X-Timestamp başlığının değeri (unix saniye)
secret: string; // whsec_…
}): boolean {
const ts = Number.parseInt(timestamp, 10);
if (!Number.isFinite(ts)) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - ts) > TOLERANCE_SECONDS) {
return false; // çok eski veya gelecekte çok ileri
}
const expected = "sha256=" + createHmac("sha256", secret)
.update(`${timestamp}.${body}`)
.digest("hex");
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}Replay koruması
İmzalanmış payload’ın içindeki timestamp ilk savunma hattıdır — bir teslim yakalayan saldırgan, tolerans pencereniz dolduktan sonra onu yeniden gönderemez.
Kemer-ve-pantolon askısı (payment.settled gibi yüksek değerli event’ler
için önerilir):
- Benzersiz bir kısıtlama ile bir tabloda
X-Deliveryüzerinde dedup yapın. Tolerans penceresi içindeki replay’ler no-op’a dönüşür — handler’ınız işi iki kez yapmadan 200 döndürür. Bu, meşru yeniden denemeler için istediğiniz aynı idempotency’dir. (X-Deliverybir teslimin her yeniden denemesinde stabildir; payload birevent_idalanı taşımaz.) - Saat sapmanızın izin verdiği en küçük toleransı kullanın. ±5 dakika önerilen varsayılandır ve çoğu NTP-senkronize filonun sürdürebileceği şeyle eşleşir. Daha sıkı olması iyidir; ±30 saniyenin altında, yavaş yukarı yönlü NTP’li ağlarda meşru teslimleri reddetmeye başlarsınız.
Bir secret’ı rotate etme
- Panel → Geliştiriciler → Webhook’lar → [uç nokta] → ⋮ → Rotate Secret.
- Yeni bir secret oluşturulur ve tam olarak bir kez gösterilir. Diyaloğu kapatmadan önce kopyalayın.
- Env var’ınızı güncelleyin ve doğrulayıcınızı 24 saat içinde yeniden dağıtın.
Kayma penceresi (çift imzalama)
Bir rotation’dan sonraki 24 saat boyunca, her teslim iki imza taşır:
X-Signature: sha256=<hmac(new_secret, ts + "." + body)>
X-Signature-Prev: sha256=<hmac(prev_secret, ts + "." + body)>
X-Timestamp: 1729536000Önceki secret’ı çalıştıran bir doğrulayıcı X-Signature-Prev ile
eşleşir; yeni secret’ı çalıştıran bir doğrulayıcı X-Signature ile
eşleşir. Her iki başlığın geçmesi yeterlidir — handler’ınız dağıtımı
tutmadan geçiş sırasında teslimi kabul edebilir.
Kayma penceresi kapandıktan sonra yalnızca X-Signature gönderilir.
Önceki secret kabul edilmeyi durdurur ve hâlâ onunla yapılandırılmış
herhangi bir doğrulayıcı teslimleri reddetmeye başlar — bu yüzden
roll-out’unuzu 24 saatlik bütçe içinde bitirin.
Önerilen alıcı deseni
// Bir rotation kayma penceresinde her iki imzayı da kabul edin.
const sig = req.headers["x-signature"] ?? "";
const sigPrev = req.headers["x-signature-prev"] ?? "";
const ok = verify(body, sig, ts, CURRENT_SECRET)
|| (PREV_SECRET && verify(body, sigPrev, ts, PREV_SECRET));Uç noktanızdaki kayma penceresi süresi dolduğunda ve PREV_SECRET’ı
env’inizden kaldırdığınızda, X-Signature-Prev dalını düşürebilirsiniz.
Acil durum iptali
Bir secret public olarak sızdıysa ve önceki secret’ı hemen geçersiz kılmanız gerekiyorsa — yani 24 saatlik çakışmanın bilinen-hatalı bir anahtarı canlı tutmasını istemiyorsanız — iki kez rotate edin. İlk rotation, sızdırılmış secret’ı prev yuvasına taşır; ikinci rotation, onu prev yuvasından çıkarır (hâlâ yeni anahtarla değiştirir), böylece sızdırılmış değer artık kabul edilmez.
Kablolamanızı test etme
Panelde, Geliştiriciler → Webhook’lar’ı açın ve doğrulamak istediğiniz uç noktada Send Test’e tıklayın. URL’ye senkron olarak sentetik bir zarf imzalar ve POST ederiz, ardından HTTP durumunu, gecikmeyi ve yanıtınızın 512 baytlık bir parçacığını gösteririz. Payload şekli:
{
"event_id": "<uuid>",
"event_type": "webhook.test.ping",
"created_at": "2026-05-17T12:00:00Z",
"test": true,
"data": {
"merchant_id": "<your-merchant-id>",
"webhook_id": "<endpoint-id>",
"message": "Test ping from the merchant dashboard..."
}
}Test ping, production teslimleri ile aynı imzalama şemasını kullanır, bu yüzden bu butondan yeşil bir onay, doğrulayıcınızın gerçek event’leri de kabul ettiğini doğrular. Test ping’leri RMQ yeniden deneme pipeline’ını atlar — yeniden denemeleri çalıştırmak istiyorsanız, ilgili API akışı üzerinden gerçek bir event tetikleyin.
Yaygın arızalar
| Belirti | Olası neden |
|---|---|
| Dev’de her zaman false döndürür | Gövde HMAC öncesinde JSON-ayrıştırıldı. Önce ham baytları okuyun. |
| Dün çalışıyordu, bugün başarısız | Secret’ı rotate ettiniz ama bu sunucudaki env var hâlâ eskisini içeriyor. Yeni secret ile yeniden dağıtın. |
| Eski event’lerde başarısız, yenilerde çalışıyor | Rotation öncesinde bir teslim kuyruğa alınmıştı; imza eski secret’ı kullanıyor ve doğrulayıcınız artık kabul etmiyor. Yeniden denemenin onu düşürmesini bekleyin veya panel üzerinden replay yapın. |
| Timestamp karşılaştırmasında bir kayma | Unix-saniyeyi unix-saniyeye karşılaştırdığınızdan emin olun. JS’deki Date.now() milisaniyedir — 1000’e bölün. |
| Yerelde çalışıyor, prod’da başarısız | Bir proxy (Cloudflare, nginx) açıyor, yeniden kodluyor veya sonundaki bir newline’ı çıkarıyor. Handler’ınızın gördüğü baytları inceleyin. |
| Test ping 401 / imza uyumsuzluğu diyor | Doğrulayıcınız yalnızca body’yi imzalıyor (2026 öncesi şema). timestamp + "." + body imzalamak için güncelleyin. |
| Başlık tamamen eksik | Uç nokta farklı bir ortam için kayıtlı. Test modu uç noktaları yalnızca environment=test event’leri alır. |