Gelen Webhook’ların Güvenlik Açığı
Stripe, Twilio veya GitHub gibi üçüncü taraf servisleri B2B SaaS sisteminize entegre ederken, webhooks kritik bir öneme sahiptir. Uygulamanızın harici olaylara (örn. başarılı bir abonelik ödemesi) anında tepki vermesini sağlar. Ancak, bu webhooks’ları almak için genel bir uç nokta açmak, iki büyük mimari riski de beraberinde getirir: Taklit Etme (Spoofing) ve Çift Teslim (Duplicate Delivery).
Gelen POST isteklerine körü körüne güveniyorsanız, kötü niyetli kişiler sahte payload’lar göndererek webhook URL’nize erişim kazanabilir. Ayrıca, webhook sağlayıcıları “en az bir kere” teslim garantisi verir. Bu da demektir ki, bir ağ kesintisi yaşandığında Stripe aynı “Ödeme Alındı” webhook’unu üç kez gönderebilir. Eğer bu durumu düzgün yönetmezseniz, kullanıcı hesabına tek bir ödeme için üç kez kredi tanımış olursunuz.
Savunma Katmanı 1: Kriptografik İmza Doğrulaması
İlk adım, payload’un gerçekten güvenilir sağlayıcıdan geldiğini doğrulamaktır. Sağlayıcılar HTTP başlıklarında bir kriptografik imza (genellikle HMAC SHA256 hash) gönderir. Bu hash’i yerelde gizli anahtarımızı kullanarak hesaplamalı ve gelen başlıkla karşılaştırmalıyız.
Bunu Laravel Middleware kullanarak, kontrolcüye ulaşmadan önce route’u koruyarak uygularız.
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class VerifyWebhookSignature
{
public function handle(Request $request, Closure $next)
{
// 1. Başlıklardan imzayı al
$signature = $request->header('X-Provider-Signature');
if (!$signature) {
throw new AccessDeniedHttpException('Imza eksik.');
}
// 2. Ham payload ve gizli anahtar kullanarak beklenen hash'i hesapla
$payload = $request->getContent();
$secret = config('services.provider.webhook_secret');
$computedSignature = hash_hmac('sha256', $payload, $secret);
// 3. Zamanlama saldırılarını önlemek için hash_equals kullan
if (!hash_equals($computedSignature, $signature)) {
throw new AccessDeniedHttpException('Geçersiz imza.');
}
return $next($request);
}
}
Savunma Katmanı 2: Redis ile İdempotens
Göndericiyi güvendikten sonra, çift işlemleri önlemeliyiz. Bunu webhook uç noktalarımızı İdempotent yaparak başarırız; yani aynı işlemi birden fazla kez uygulamak, yalnızca bir kez uygulamakla aynı sonucu vermelidir.
Her webhook payload’u benzersiz bir Event ID içerir. Bu ID’yi kilitlemek için Laravel’in Cache (Redis destekli) kullanıyoruz. Eğer aynı ID’yi tekrar görürsek, sağlayıcıya (HTTP 200) alındığını bildirebiliriz fakat içsel işlemlerimizi atlayabiliriz.
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class WebhookController extends Controller
{
public function handlePaymentEvent(Request $request)
{
$eventId = $request->input('event_id');
// Cache::add() kullanarak bir Redis atomik kilidi oluştur
// Anahtar yoksa (ilk kez), true döner, varsa false döner.
// Redis'i temiz tutmak için 24 saat TTL ayarlıyoruz.
$isFirstTime = Cache::add("webhook_processed_{$eventId}", true, now()->addHours(24));
if (!$isFirstTime) {
// Bu tam webhook'u zaten işledik. Onayla ve abort et.
Log::info("Atlanan kopya webhook: {$eventId}");
return response()->json(['status' => 'ignored', 'reason' => 'duplicate']);
}
// Karmaşık iş mantığına devam et (örn. kiracı faturalama durumunu güncelleme)
// ...
return response()->json(['status' => 'success']);
}
}
Sonuç
Dayanıklı SaaS platformları, savunma mühendisliği üzerine inşa edilmiştir. İmza doğrulama middleware’ini ve Redis destekli idempotans kilitlerini mimarlayarak, kırılgan ve savunmasız API uç noktalarını, güvenli bir şekilde milyonlarca harici olayı işleyebilen kurumsal düzeyde webhooks’a dönüştürürsünüz.
Kaynak: Orijinal Makale


