Ödeme Webhook'u Güvenliği: HMAC İmza Doğrulama ve Idempotency Rehberi
Webhook, ödeme sağlayıcısının sunucunuza 'para geldi' demesidir. Bu mesajı doğrulamadan siparişi onaylarsanız, webhook adresinizi bilen herkes bedava alışveriş yapabilir. Bu rehber doğru işlemenin her adımını, üç dilde örnekle anlatır.
Webhook neden doğrulanmalı?
Webhook ucunuz herkese açık bir HTTPS adresidir; öyle olmak zorundadır, yoksa sağlayıcı size ulaşamaz. Bu adresi bilen biri kendi bilgisayarından {"status":"PAID","invoice_id":"..."} gövdeli bir POST gönderirse sunucunuz bunu gerçek ödeme sanır. Adres tahmin edilebilir (/webhook, /kriptogo-webhook), ödeme sayfası fatura kimliğini herkese gösterir. Yani saldırının tüm malzemesi ortadadır. Tek koruma imzadır.
HMAC imza nasıl çalışır?
KriptoGo her webhook gövdesini, yalnızca sizin ve KriptoGo'nun bildiği secretKey ile HMAC-SHA256 algoritmasından geçirir ve sonucu X-Kriptogo-Signature başlığında gönderir. Siz aynı işlemi kendi tarafınızda yapar, iki değeri karşılaştırırsınız. Secret'ı bilmeyen biri geçerli imza üretemez; gövdenin tek bir karakteri değişse imza tutmaz. Secret hiçbir istekte gönderilmez, yalnızca imza hesaplamada kullanılır.
Ham gövde sorunu
En sık yapılan hata şudur: framework gövdeyi otomatik olarak JSON nesnesine çevirir, geliştirici JSON.stringify(req.body) ile geri metne dönüştürüp imzalar. Ama bu yeni metin, gelen ham metinle byte byte aynı olmayabilir: boşluklar, alan sırası, sayı biçimi (49.90 → 49.9) değişir. İmza tutmaz.
Çözüm: imzayı sunucuya gelen ham byte'lar üzerinden hesaplamak. Her framework'te bunun bir yolu vardır: Express'te express.json({ verify }), Next.js App Router'da await req.text(), PHP'de php://input, Flask'ta request.get_data(), Django'da request.body.
Sabit zamanlı karşılaştırma
İki imzayı === ile karşılaştırmak, karşılaştırmanın ilk farklı karakterde durması nedeniyle teorik olarak zamanlama saldırısına açıktır. Pratikte bu uzaktan sömürülmesi zor bir zayıflıktır, ancak düzeltmesi tek satırdır: crypto.timingSafeEqual (Node.js), hash_equals (PHP), hmac.compare_digest (Python). Uzunlukları önce kontrol edin; farklı uzunlukta buffer'lar bazı fonksiyonlarda hata fırlatır.
Node.js, PHP, Python örnekleri
app.use('/kriptogo-webhook', express.json({ verify: (req, res, buf) => { req.rawBody = buf.toString('utf8'); } })); app.post('/kriptogo-webhook', (req, res) => { const expected = crypto.createHmac('sha256', process.env.KRIPTOGO_SECRET_KEY).update(req.rawBody).digest('hex'); const got = req.get('X-Kriptogo-Signature') || ''; const ok = got.length === expected.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected)); if (!ok) return res.status(401).send('invalid signature'); res.sendStatus(200); handlePayment(req.body).catch(console.error); // arka planda });
$raw = file_get_contents('php://input'); $received = $_SERVER['HTTP_X_KRIPTOGO_SIGNATURE'] ?? ''; $expected = hash_hmac('sha256', $raw, getenv('KRIPTOGO_SECRET_KEY')); if (!hash_equals($expected, $received)) { http_response_code(401); exit; } http_response_code(200); $data = json_decode($raw, true); // $data['invoice_id'], $data['status'], $data['paid_amount']
@app.post("/kriptogo-webhook") def webhook(): raw = request.get_data() expected = hmac.new(os.environ["KRIPTOGO_SECRET_KEY"].encode(), raw, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, request.headers.get("X-Kriptogo-Signature", "")): return "invalid signature", 401 data = request.get_json() return "ok", 200
Yeniden deneme ve idempotency
Sunucunuz 10 saniye içinde 200 dönmezse KriptoGo bildirimi 1 ve 2 saniye arayla toplamda üç kez gönderir. Bu iyi bir şeydir: geçici bir kesintide ödemeyi kaçırmazsınız. Ama sonucu da kabul etmelisiniz: aynı bildirim birden fazla kez gelebilir. İlk deneme aslında ulaşmış ama yanıt zaman aşımına düşmüş olabilir.
Çözüm idempotency'dir: siparişi invoice_id ile bulun; zaten "ödendi" ise hiçbir şey yapmadan 200 dönün. Veritabanında invoice_id sütununa benzersizlik kısıtı koymak ve durum güncellemesini "yalnızca hâlâ bekliyorsa" koşuluyla yapmak (UPDATE ... WHERE status = 'pending') yarış durumlarını da kapatır.
Hızlı yanıt, arka planda iş
E-posta göndermek, stok düşmek, PDF üretmek saniyeler alabilir. Bunları yanıttan önce yaparsanız 10 saniyelik süreyi aşar, gereksiz yeniden denemeler tetiklersiniz. Doğru sıra: imzayı doğrula → 200 dön → işi kuyruğa at veya arka planda çalıştır. Express örneğinde res.sendStatus(200)'ın ardından handlePayment çağrısının beklenmemesi bu yüzdendir.
Kontrol listesi
- İmza ham gövde üzerinden hesaplanıyor.
- Karşılaştırma sabit zamanlı fonksiyonla yapılıyor.
- Geçersiz imzada 401 dönüyor ve hiçbir şey yapılmıyor.
- Aynı
invoice_idikinci kez gelince sipariş tekrar işlenmiyor. paid_amount, sipariş tutarıyla karşılaştırılıyor.- Uzun işler yanıttan sonra çalışıyor.
- Webhook adresi yalnızca HTTPS ve herkese açık; KriptoGo özel ağ adreslerini zaten reddeder.
- Secret ortam değişkeninde; loglara yazılmıyor.
Uçtan uca akış için adım adım rehbere, tüm alanlar için dökümana bakın.
Next.js App Router örneği
Next.js'te gövdeyi otomatik ayrıştıran bir katman yoktur; req.text() ham metni verir. Route'un Node çalışma zamanında olması gerekir çünkü node:crypto kullanılır.
import crypto from 'node:crypto'; export const runtime = 'nodejs'; export async function POST(req: Request) { const raw = await req.text(); // ham gövde const expected = crypto.createHmac('sha256', process.env.KRIPTOGO_SECRET_KEY!).update(raw).digest('hex'); const got = req.headers.get('x-kriptogo-signature') ?? ''; if (got.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))) { return new Response('invalid signature', { status: 401 }); } const data = JSON.parse(raw); if (data.status === 'PAID') await markOrderPaid(data.invoice_id, data.paid_amount); // idempotent return new Response('ok'); }
Test: sahte imza ve tekrar gönderim
Doğrulamanın çalıştığını iki basit testle kanıtlayın. Birincisi yanlış imza: 401 dönmeli ve veritabanında hiçbir şey değişmemeli. İkincisi tekrar gönderim: aynı geçerli isteği iki kez gönderin; sipariş bir kez işlenmeli.
# 1) Sahte imza → 401 beklenir curl -s -o /dev/null -w "%{http_code} " -X POST https://siten.com/kriptogo-webhook -H "Content-Type: application/json" -H "X-Kriptogo-Signature: deadbeef" -d '{"event":"payment.confirmed","invoice_id":"test","status":"PAID"}' # 2) Geçerli imza üret (secret'ı kendi ortamınızdan alın) ve iki kez gönder BODY='{"event":"payment.confirmed","invoice_id":"test-1","status":"PAID","amount":"1","paid_amount":"1"}' SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$KRIPTOGO_SECRET_KEY" | awk '{print $2}') for i in 1 2; do curl -s -X POST https://siten.com/kriptogo-webhook -H "Content-Type: application/json" -H "X-Kriptogo-Signature: $SIG" -d "$BODY"; done
Not: printf '%s' kullanın; echo sona yeni satır ekler ve imza tutmaz. Bu, "ham gövde" kuralının komut satırındaki karşılığıdır.
Loglama ve izleme
- Her webhook için
invoice_id, doğrulama sonucu (geçti/geçmedi) ve işlem süresini loglayın. Gövdenin tamamını ve secret'ı loglamayın. - Geçersiz imza sayısında ani artış, adresinizin keşfedildiğini gösterir; alarm kurun.
- KriptoGo panelinde faturanın "ödendi" olduğu ama sizde siparişin açık kaldığı durumlar için günlük bir mutabakat sorgusu çalıştırın:
GET /api/status/:idile açık siparişleri kontrol edin. - Webhook ucunuzun yanıt süresini izleyin; 10 saniyeye yaklaşıyorsa işi arka plana taşıyın.
Sık hatalar ve belirtileri
| Belirti | Muhtemel neden | Çözüm |
|---|---|---|
| Tüm webhook'lar 401 | İmza ham gövde yerine yeniden serileştirilmiş JSON üzerinden hesaplanıyor; ya da yanlış secret | Ham gövdeyi kullan; panelden secret'ı yeniden kopyala |
| Ürün iki kez teslim edildi | Yeniden deneme + idempotency yok | Sipariş durumunu kontrol et; WHERE status='pending' ile güncelle |
| Webhook hiç gelmiyor | Adres panelde kayıtlı değil; HTTP (TLS'siz); güvenlik duvarı | HTTPS adresi panele kaydet; sunucu loglarını kontrol et |
| Bazen 200 bazen zaman aşımı | Yanıttan önce e-posta/PDF işleri | Önce 200, sonra arka plan |
| Fatura ödendi ama sipariş tutarı tutmuyor | Eksik ödeme toleransı | paid_amount karşılaştır; eksikse beklemeye al |
Sık sorulan sorular
Webhook alamıyorsam ne yaparım?
Yerel geliştirme gibi dışarıya açık adresi olmayan ortamlarda GET /api/status/:id ucunu birkaç saniyede bir sorgulayabilirsiniz. Üretimde ise webhook adresinin panelde doğru kayıtlı olduğunu, HTTPS olduğunu ve sunucunun 200 döndüğünü kontrol edin.
Aynı webhook iki kez geldi, bu hata mı?
Hayır, yeniden deneme mekanizmasının doğal sonucudur. Sunucunuz idempotent olmalı: aynı invoice_id için siparişi ikinci kez işlememeli.
İmza başlığında sha256= öneki var mı?
İki başlık gönderilir: X-Kriptogo-Signature (yalnızca hex) ve X-Kriptogo-Signature-256 (sha256= önekli). Hangisini kullandığınıza göre öneki dikkate alın.
KriptoGo ile 30 dakikada kripto ödeme al
Kurulum ücreti yok, aylık ücret yok. Yalnızca başarılı tahsilattan %1.
