Webhook İmzası Nasıl Doğrulanır? HMAC-SHA256 Rehberi
Webhook imzası doğrulama rehberi: HMAC-SHA256, t=…,v1=… başlığı, ham gövde, sabit zamanlı karşılaştırma ve replay saldırılarına karşı koruma örneklerle.

Webhook imzası doğrulamak, sunucunuza gelen bir ödeme bildiriminin gerçekten ödeme sağlayıcınızdan geldiğini ve yolda değiştirilmediğini kanıtlamanın yoludur. İmza kontrolü yapılmayan bir webhook uç noktasına herkes sahte bir "ödeme alındı" isteği gönderebilir. Bu rehberde HMAC-SHA256 ile webhook imzasının nasıl doğrulanacağını, sık yapılan hataları ve replay saldırılarına karşı korumayı anlatıyoruz.
Webhook imzası neden gerekli?
Webhook uç noktanız internete açık bir URL'dir, örneğin https://magazam.com/mercanpay/webhook. Bu adresi bilen herkes buraya şöyle bir istek gönderebilir:
{ "type": "invoice.paid", "data": { "invoice": { "order_id": "order-1001", "status": "paid" } } }
İmza doğrulaması yapmıyorsanız sisteminiz bu siparişi ödenmiş sayar ve ürünü teslim eder. Hiç ödeme yapılmadığı hâlde. Bu, kripto ödeme entegrasyonlarında en pahalıya patlayan hatalardan biridir.
İmza bu sorunu çözer. Ödeme sağlayıcısı ile sizin aranızda paylaşılan gizli bir anahtar (sır) vardır. Sağlayıcı her isteği bu sırla imzalar. Sırrı bilmeyen biri geçerli bir imza üretemez.
HMAC-SHA256 nasıl çalışır?
HMAC (Hash-based Message Authentication Code), bir mesajı ve gizli bir anahtarı birlikte özetleyen bir yöntemdir. SHA-256 ise kullanılan özet (hash) fonksiyonudur. Sonuç şu özelliklere sahiptir:
- Aynı mesaj ve aynı sır her zaman aynı imzayı üretir.
- Mesajda tek bir bayt değişirse imza tamamen değişir.
- Sırrı bilmeden geçerli imza üretmek pratikte imkânsızdır.
MercanPay'de imza, HMAC-SHA256(sır, zaman_damgası + "." + ham_gövde) değerinin hex hâlidir. Sonuç X-MercanPay-Signature başlığında şu biçimde gönderilir: t=1790000052,v1=5f2b...e91a.
Burada t Unix zaman damgası (saniye), v1 ise imzanın hex hâlidir. Sır, panelde Ayarlar sayfasında bulunan ve whsec_ ile başlayan webhook imza sırrıdır.
Webhook imzası doğrulama: 5 adım
- Ham gövdeyi okuyun. İsteğin gövdesini JSON olarak ayrıştırmadan, bayt bayt alın.
- Başlığı ayrıştırın.
tvev1değerlerini virgülle ayrılmış parçalardan çıkarın. - Zaman damgasını kontrol edin.
tdeğeri şimdiki zamandan 5 dakikadan fazla uzaksa isteği reddedin. - Beklenen imzayı hesaplayın.
t + "." + ham_gövdemetnini sırrınızla HMAC-SHA256'dan geçirin. - Sabit zamanlı karşılaştırın. Hesapladığınız imzayı
v1ile, zamanlamaya dayalı saldırılara açık olmayan bir fonksiyonla karşılaştırın.
İmza geçerliyse gövdeyi JSON olarak ayrıştırıp olayı işleyin. Geçersizse 401 döndürün ve hiçbir işlem yapmayın.
PHP ile doğrulama
<?php
$secret = getenv('MERCANPAY_WEBHOOK_SECRET'); // whsec_...
$body = file_get_contents('php://input'); // ham gövde
$header = $_SERVER['HTTP_X_MERCANPAY_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $sig); // t=...,v1=...
$fresh = isset($sig['t']) && abs(time() - (int) $sig['t']) <= 300;
$mac = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $body, $secret);
if (!$fresh || !hash_equals($mac, $sig['v1'] ?? '')) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
Resmi kütüphaneyi kullanıyorsanız (composer require mercanpay/mercanpay-php) tüm bunlar tek satırdır:
if (!\MercanPay\MercanPay::verifyWebhook($secret, $body, $header)) { http_response_code(401); exit; }
Python ile doğrulama
import hashlib, hmac, time
def verify(secret: str, body: bytes, header: str, tolerance: int = 300) -> bool:
sig = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
t = int(sig.get("t", 0))
if "v1" not in sig or abs(time.time() - t) > tolerance:
return False
mac = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, sig["v1"])
pip install mercanpay ile gelen verify_webhook(secret, body, header) fonksiyonu aynı işi yapar. Flask ve Django örnekleri için Python entegrasyon rehberine bakabilirsiniz.
Node.js ile doğrulama
import crypto from 'node:crypto';
import express from 'express';
function verify(secret, rawBody, header, tolerance = 300) {
const sig = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(sig.t);
if (!sig.v1 || !Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > tolerance) return false;
const mac = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return mac.length === sig.v1.length && crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(sig.v1));
}
const app = express();
// imza ham gövde üzerinden hesaplanır: bu rotada express.json() kullanmayın
app.post('/mercanpay/webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!verify(process.env.MERCANPAY_WEBHOOK_SECRET, req.body.toString(), req.get('X-MercanPay-Signature') || '')) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
res.sendStatus(200);
});
Sık yapılan hatalar
| Hata | Sonucu | Doğrusu |
|---|---|---|
| Gövdeyi JSON'a çevirip yeniden serileştirmek | Boşluk ve sıra değişir, imza tutmaz | Ham baytları kullanın |
== ile karşılaştırmak |
Zamanlama saldırısına açık | hash_equals, hmac.compare_digest, timingSafeEqual |
| Zaman damgasını kontrol etmemek | Eski istekler tekrar oynatılabilir | 5 dakikalık tolerans uygulayın |
| Sırrı koda gömmek | Depo sızarsa sır da sızar | Ortam değişkeni kullanın |
| İmza hatasında 200 dönmek | Hata gizlenir | 401 dönün ve loglayın |
Siparişi success_url yönlendirmesine göre onaylamak |
URL taklit edilebilir | Yalnızca imzalı webhook veya API |
Replay saldırılarına karşı koruma
Geçerli bir webhook isteğini ele geçiren biri, onu daha sonra tekrar gönderebilir. Buna replay (tekrar oynatma) saldırısı denir. İmza geçerli olduğu için yalnızca imza kontrolü bunu durduramaz. Koruma iki katmandan oluşur:
- Zaman penceresi: İmzaya zaman damgası da dahil olduğu için saldırgan
tdeğerini değiştiremez. 5 dakikadan eski istekleri reddettiğinizde eski bir istek işe yaramaz. - Olay kimliğiyle tekrar eleme: Her webhook gövdesinde benzersiz bir
idalanı bulunur. İşlediğiniz olay kimliklerini benzersiz bir veritabanı sütununda saklayın ve daha önce gördüğünüz kimliği tekrar işlemeyin.
İkinci katman yalnızca saldırılara karşı değil, meşru tekrarlara karşı da gereklidir. Sunucunuz 10 saniye içinde 2xx dönmezse MercanPay isteği artan aralıklarla yeniden gönderir: 30 sn, 2 dk, 10 dk, 30 dk, 1 sa, 3 sa, 6 sa, 12 sa ve 24 sa. Aynı olay size birkaç kez ulaşabilir. Kodunuz bunu sorunsuz karşılamalıdır.
İmzadan sonra: iş mantığı kontrolleri
İmza, isteğin MercanPay'den geldiğini kanıtlar. Siparişi onaylamadan önce yine de şunları kontrol edin:
data.invoice.order_idsizin sisteminizde gerçekten var mı?tokenvepaid_amountsiparişinizle uyumlu mu?- Olay türü
invoice.paidveyainvoice.overpaidmı?invoice.partialbir eksik ödemedir ve sipariş henüz tamamlanmamıştır.
Eksik ve fazla ödemelerin nasıl ele alınacağını eksik, fazla ve geç ödemeler yazısında anlattık.
Sık sorulan sorular
Webhook'umu nasıl test ederim?
Panelde Webhooklar sayfasındaki "Test gönder" butonu imzalı bir test.ping olayı gönderir. Teslimat geçmişinde her isteğin yanıt kodunu ve süresini görebilir, dilediğiniz olayı tekrar gönderebilirsiniz.
Neden 5 dakikalık tolerans?
Sunucu saatleri arasında küçük farklar olabilir. 5 dakika, meşru istekleri reddetmeyecek kadar geniş, eski isteklerin tekrar oynatılmasını engelleyecek kadar dardır. Sunucu saatinizin NTP ile senkron olduğundan emin olun.
HTTPS kullanıyorsam imzaya yine de gerek var mı?
Evet. HTTPS trafiği yolda şifreler, ancak isteği kimin gönderdiğini kanıtlamaz. Uç noktanıza herkes HTTPS üzerinden istek atabilir.
MercanPay ile başlayın
MercanPay webhook'ları HMAC-SHA256 ile imzalanır, otomatik olarak yeniden denenir ve panelde teslimat geçmişiyle izlenebilir. PHP ile başlamak için PHP entegrasyon rehberine, tüm uç noktalar için dokümantasyona göz atın. Satıcı hesabınızı oluşturun.


