1. Webhook Neden E-Ticaretin En Hassas ve En Tehlikeli Kapısıdır?
Müşteri banka sayfasına yönlendirildiğinde (3D Secure SMS şifresini girdiğinde), tarayıcıyı kapatabilir veya elektrik kesilebilir. Bu yüzden modern ödeme sağlayıcıları (Stripe, PayTR, İyzico) ödeme sonucunu doğrudan tarayıcıya güvenerek değil; sunucudan sunucuya fırlatılan Webhook çağrısı ile bildirir.
Ancak webhook uç noktanız (/api/v1/payments/webhook) internete tamamen açık bir kapıdır. Eğer doğru savunma mimarisi kurulmazsa:
- Bir saldırgan bu uç noktaya sahte
{"status": "SUCCESS", "orderId": "123"}JSON gövdeleri atarak hiç para ödemeden siparişini onaylatabilir! - Banka ağı dalgalandığında aynı bildirim arka arkaya 4 kez gönderilebilir. Eğer kodunuz Idempotent değilse, müşteriye 4 kez sipariş açılır veya stok 4 kez mükerrer düşülür.
2. HMAC-SHA256 Kriptografik İmza Doğrulaması
Stilmerce, gelen webhook isteklerinin gerçekten Stripe, PayTR veya İyzico'dan geldiğini matematiksel olarak kanıtlamak için HMAC (Hash-based Message Authentication Code) doğrulaması yapar:
// payment-webhook.controller.ts
import * as crypto from 'crypto';
import { Controller, Post, Headers, Req, BadRequestException } from '@nestjs/common';
@Controller('v1/payments')
export class PaymentWebhookController {
@Post('paytr/callback')
async handlePaytrCallback(@Headers('x-paytr-token') incomingToken: string, @Req() req: Request) {
const { merchant_oid, status, total_amount } = req.body;
const merchantKey = process.env.PAYTR_MERCHANT_KEY;
const merchantSalt = process.env.PAYTR_MERCHANT_SALT;
// 1. Banka ile paylaşılan gizli anahtarla beklenen hash'i yerel olarak hesapla
const payload = `${merchant_oid}${merchantSalt}${status}${total_amount}`;
const expectedToken = crypto.createHmac('sha256', merchantKey).update(payload).digest('base64');
// 2. Zamanlama saldırılarını (Timing Attack) önlemek için timingSafeEqual kullan
const isAuthentic = crypto.timingSafeEqual(
Buffer.from(incomingToken),
Buffer.from(expectedToken)
);
if (!isAuthentic) {
throw new BadRequestException('Sahte webhook isteği tespit edildi!');
}
// 3. Güvenli akışa devam et
}
}
Neden timingSafeEqual?
Standart incomingToken === expectedToken karşılaştırması ilk farklı karakterde döner. Bir saldırgan istek gecikmelerini nanisaniyeler seviyesinde ölçerek doğru hash'i karakter karakter tahmin edebilir (Timing Attack). crypto.timingSafeEqual her zaman sabit sürede çalışarak bu açığı imkansız kılar.
3. Idempotency Key (Tekillik Kalkanı): Mükerrer İşlemleri Sıfırlama
Ağ paketlerinin tekrar gönderilmesi (Network Retry) durumunda aynı ödemenin iki kez işlenmesini engellemek için Redis Dağıtık Kilit & İşlem Durumu Tablosu kullanılır:
// idempotency.guard.ts
async processPaymentWebhook(transactionId: string, processFn: () => Promise<void>) {
// 1. Bu işlem daha önce tamamlandı mı?
const isProcessed = await this.redis.get(`processed_tx:${transactionId}`);
if (isProcessed) {
// Zaten işlenmiş: Doğrudan HTTP 200 dön ve işlemi tekrarlama!
return { status: 'ALREADY_PROCESSED' };
}
// 2. Atomik kilit alarak eşzamanlı ikinci bildirimi engelle
const lock = await this.redis.set(`lock:tx:${transactionId}`, 'LOCKED', 'NX', 'EX', 10);
if (!lock) {
return { status: 'IN_PROGRESS' };
}
try {
await processFn(); // Siparişi oluştur, faturayı tetikle
await this.redis.setex(`processed_tx:${transactionId}`, 86400 * 30, 'SUCCESS');
} finally {
await this.redis.del(`lock:tx:${transactionId}`);
}
}