Alışveriş Sepeti

Sepetiniz boş

Güvenlik & Altyapı · 16 dk

Ödeme Geçitleri & Webhook Güvenliği: Stripe, PayTR ve İyzico Entegrasyonlarında Sıfır Hata

Sahte bildirimleri engelleyen HMAC-SHA256 imza doğrulaması, mükerrer tahsilatları önleyen Idempotency anahtarları ve 3D Secure akışının kurumsal mimari rehberi.

Yazar: Stilgen Core Engineering

Ödeme Geçitleri & Webhook Güvenliği: Stripe, PayTR ve İyzico Entegrasyonlarında Sıfır Hata

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}`);
  }
}