Merhaba, önceki dersimizde bir kaynağın durumunun kontrol edildiği an (Check) ile kullanıldığı an (Use) arasındaki zaman boşluğunu (Gap) istismar eden TOCTOU zafiyetlerini derinlemesine inceledik. Bu zafiyetlerin, özellikle dosya sistemi manipülasyonları ve çok adımlı yetkilendirme akışlarında nasıl kritik riskler oluşturduğunu gördük. O dersin odak noktası, bir işlem başlamadan önce sistemin durumunun bütünlüğünü korumaktı.
Bu dersimizde ise madalyonun diğer yüzüne, yani bir işlem başladıktan sonra işler ters giderse ne olacağına odaklanacağız. Öğrenme hedefimiz, hata yönetimi ve yeniden deneme mantığında istismar edilebilir durum tutarsızlıkları ve kısmi geri alma açıklarını analiz etmektir. Modern dağıtık sistemlerde tek bir mantıksal işlem, birden çok bağımsız ve potansiyel olarak başarısız olabilecek adımdan oluşur. Bu adımların başarısızlık anında nasıl yönetildiği, uygulamanın güvenliği için en az başarılı olduğu durumdaki mantığı kadar kritiktir.
Kısmi Başarısızlık ve Durum Tutarsızlığı Problemi
Karmaşık iş akışları, doğaları gereği "kısmi başarısızlık" (partial failure) riskini barındırır. Bir banka havalesi gibi klasik bir örneği düşünelim:
- A hesabından X miktar parayı düş (Debit).
- B hesabına X miktar parayı ekle (Credit).
Bu iki adım, tek bir mantıksal işlemin parçalarıdır. Peki, 1. adım başarılı olurken bir ağ sorunu nedeniyle 2. adım başarısız olursa ne olur? Sistem, paranın A hesabından çıktığı ancak B hesabına hiç ulaşmadığı, "tutarsız bir duruma" (inconsistent state) girer. Bu durum, muhasebe kayıtlarında bir "kayıp" para demektir.
Güvenlik açısından bakıldığında, bu tür tutarsız durumlar genellikle istismar edilebilir zafiyetlere yol açar. Örneğin, bir kullanıcının yetkilerini yükselten bir işlemin ilk adımı başarılı olur, ancak işlemi loglayan ikinci adım başarısız olursa, sistemde hem yetkisiz bir eylem gerçekleşmiş hem de bu eylemin izi kaybolmuş olabilir.
Bu soruna karşı en yaygın savunma mekanizması, işlemi "geri almaktır" (rollback). Yukarıdaki havale örneğinde, 2. adım başarısız olduğunda, sistemin 1. adımı geri alarak A hesabına parayı iade etmesi beklenir. Ancak, ya geri alma işlemi de başarısız olursa? İşte bu noktada, "kısmi geri alma" (partial rollback) adı verilen ve genellikle gözden kaçan tehlikeli bir durum ortaya çıkar.
Yeniden Deneme (Retry) Mantığının Tehlikeleri
Geçici hatalarla (transient failures) başa çıkmanın bir diğer popüler yolu ise işlemi yeniden denemektir. Özellikle mikroservis mimarilerinde, geçici ağ sorunları veya bir servisin anlık yoğunluğu nedeniyle başarısız olan çağrıları birkaç kez daha denemek yaygın bir pratiktir. Ancak bu mantık, dikkatsizce uygulandığında ciddi güvenlik açıklarına yol açabilir.
Temel sorun, yeniden denenen işlemin idempotent olup olmamasında yatar.
- Idempotent İşlem: Bir işlemin bir veya birden çok kez tekrarlanmasının, sonucunu değiştirmemesidir. Örneğin, bir dosyayı silmek (
DELETE /files/abc.txt) idempotenttir. Dosya ilk çağrıda silinir, sonraki çağrılar "dosya bulunamadı" hatası verse de sistemin nihai durumu değişmez. - Non-idempotent İşlem: Her tekrarlandığında sistemin durumunu değiştiren işlemdir. Örneğin, bir hesaba 100 TL ekleyen bir API çağrısı (
POST /accounts/123/deposit?amount=100) non-idempotenttir. Bu çağrıyı üç kez yeniden denemek, hesaba 300 TL eklenmesiyle sonuçlanır.
Aşağıdaki akış şeması, tipik bir yeniden deneme mantığını göstermektedir.

Bu şemaya bir kod denetçisi gözüyle baktığımızda sormamız gereken ilk soru şudur: "Yeniden denenen 'Attempt Call' adımı idempotent mi?" Eğer değilse, bu basit mantık, yukarıda bahsettiğimiz gibi yinelenen işlemlere yol açarak veri bütünlüğünü bozabilir ve finansal kayıplara neden olabilir.
Daha gelişmiş sistemler, ne tür hataların yeniden denenebileceğini ayırt etmeye çalışır.
Bu daha olgun model bile kritik soruları beraberinde getirir:
- "Retriable Exception" listesi nasıl tanımlanmış? Örneğin, bir "veritabanı bağlantı hatası" yeniden denenebilirken, "geçersiz kupon kodu" gibi bir iş mantığı hatasının yeniden denenmesi anlamsızdır ve sistemde gereksiz yük yaratır. Bu listenin yanlış yapılandırılması, bir saldırganın sistemi DoS saldırısıyla yormasına olanak tanıyabilir.
- "Max Retries Reached" durumunda tetiklenen "Retry Processing" ne anlama geliyor? İşlemi daha sonra denemek üzere bir kuyruğa mı atıyor (dead-letter queue)? Bir yöneticiye uyarı mı gönderiyor? Yoksa işlemi geri almaya mı çalışıyor? Güvenlik açıkları genellikle bu belirsiz ve yeterince test edilmemiş hata yollarında gizlenir.
Kodda Aranacak Zafiyet Desenleri
Desen 1: Başarısız Geri Alma (The Failed Rollback)
Çok adımlı bir işlemin ortasında hata oluştuğunda, ideal olarak bir geri alma (rollback) mantığı çalışır. Ancak bu geri alma mantığının kendisi de başarısız olabilir.
Senaryo: Yeni bir kullanıcı kaydı.
userstablosuna yeni kullanıcı kaydı oluşturulur.billingservisine API çağrısı yapılarak kullanıcı için bir abonelik profili oluşturulur.
Eğer 2. adım (billing servisi meşgul olduğu için) başarısız olursa, hata yakalama bloğu (catch) çalışır ve 1. adımda oluşturulan kullanıcı kaydını silmeye çalışır. Peki, tam o anda veritabanı bağlantısı koparsa ve kullanıcıyı silme işlemi de başarısız olursa?
Sonuç: Sistemde, faturalandırma profili olmayan "hayalet" bir kullanıcı kalır. Bu kullanıcı, sisteme giriş yapabilir ve belki de hiçbir zaman faturalandırılamayacak hizmetleri kullanabilir.
Kod Analizi Stratejisi:
Birden çok, birbirine bağımlı kaynak (veritabanı, harici API, dosya sistemi) üzerinde işlem yapan try...catch bloklarını arayın. catch bloğunun içini dikkatle inceleyin:
- Bir geri alma işlemi deniyor mu?
- Geri alma işleminin kendisi ayrı bir
try...catchile sarmalanmış mı? - Geri almanın da başarısız olduğu "en kötü durum" için bir strateji var mı? Örneğin, başarısız olan işlemi ve geri alınamayan adımları kalıcı bir "telafi kuyruğuna" (compensation queue) yazarak daha sonra manuel veya otomatik olarak düzeltilmesini sağlıyor mu?
Desen 2: Yinelenen İşlemle Durum İstismarı
Bu, non-idempotent operasyonların yeniden denenmesiyle ortaya çıkan klasik bir zafiyettir.
Senaryo: "İndirim Kuponu Kullan" fonksiyonu.
Yanlış implementasyon:
- Kullanıcının sepetine indirimi uygula.
- Kuponu veritabanında "kullanıldı" olarak işaretle.
Eğer 2. adım bir veritabanı hatası nedeniyle başarısız olursa ve tüm fonksiyon yeniden denenirse ne olur? İndirim, sepete ikinci kez uygulanır. Saldırgan, veritabanını meşgul ederek bu hatayı kasıtlı olarak tetikleyebilir ve bir kuponla birden fazla indirim alabilir.
Doğru implementasyon sırası (1. kuponu işaretle, 2. indirimi uygula) bile başka bir soruna yol açabilir. Eğer indirim uygulama adımı başarısız olur ve yeniden deneme mekanizması yoksa, kullanıcının kuponu yanmış olur ama indirimi alamaz.
Kod Analizi Stratejisi:
Bir yeniden deneme döngüsü (for, while) veya Polly (C#), Tenacity (Python) gibi bir yeniden deneme kütüphanesi gördüğünüzde şu soruları sorun:
- Yeniden denenen işlem bloğu non-idempotent mi? (Örn: para transferi, sayaç artırma, listeye eleman ekleme)
- Eğer öyleyse, yinelenen işlemleri önlemek için bir mekanizma var mı? En yaygın ve sağlam çözüm, her işlem talebiyle birlikte benzersiz bir idempotency key göndermektir. Sunucu, bu anahtarı daha önce işleyip işlemediğini kontrol eder ve eğer işlediyse yeni bir işlem yapmadan önceki sonucu döndürür.
Desen 3: İstisna Yutma ve "Fail-Open"
Bu, geliştiricilerin tembellik veya dikkatsizlik sonucu düştüğü en tehlikeli tuzaklardan biridir. Kritik bir güvenlik kontrolü, bir try...catch bloğu içine alınır ve catch bloğunda hata sadece loglanır, ardından program normal akışına devam eder.
Örnek (Java):
public void performSensitiveAction(User user, Resource resource) {
try {
// Uzak bir yetkilendirme servisini çağır
authorizationService.checkPermission(user, resource.getId());
} catch (Exception e) {
// Hata! Ama ne yapalım, devam edelim...
log.error("Yetki kontrolü sırasında hata oluştu: " + e.getMessage());
}
// Yetki kontrolü başarılı varsayılarak hassas işleme devam edilir
resource.execute();
}
Eğer authorizationService ulaşılamaz durumdaysa, checkPermission bir istisna fırlatır. Bu istisna catch bloğunda yakalanır, loglanır ve program sanki her şey yolundaymış gibi resource.execute() metodunu çağırır. Bu, "fail-open" olarak bilinen ve yetkilendirme kontrollerinin tamamen devre dışı kalmasına neden olan kritik bir zafiyettir.
Kod Analizi Stratejisi:
Kod tabanında içi boş catch (Exception e) {} bloklarını veya sadece loglama yapıp akışa devam eden catch bloklarını arayın. Bu desen, özellikle kimlik doğrulama, yetkilendirme, girdi doğrulama gibi güvenlik açısından hassas fonksiyonların etrafında bulunduğunda kırmızı alarmdır. Güvenlik kontrolleri için varsayılan davranış her zaman fail-closed olmalıdır; yani, bir kontrol başarıyla tamamlanamazsa erişim reddedilmelidir.
Sonuç
Bu derste, bir uygulamanın sağlamlığının ve güvenliğinin, sadece başarılı senaryoları ne kadar iyi işlediğiyle değil, aynı zamanda başarısızlık anlarını ne kadar zarif ve güvenli bir şekilde yönettiğiyle ölçüldüğünü gördük.
Temel Çıkarımlar:
- Durum Tutarsızlığı: Çok adımlı işlemler, kısmi başarısızlıklar sonucu sistem durumunu tutarsız bırakabilir. Bu durumlar genellikle güvenlik açıklarına zemin hazırlar.
- İdempotentlik: Yeniden deneme mantığı uygulamadan önce, işlemin idempotent olduğundan emin olunmalıdır. Non-idempotent işlemler için yinelenen yürütmeyi önlemek amacıyla "idempotency key" gibi mekanizmalar kullanılmalıdır.
- Atomik Geri Almalar: Geri alma (rollback) mantığı, işlemin kendisi kadar önemlidir ve onun da başarısız olabileceği varsayılarak tasarlanmalıdır. Başarısız geri almalar için "telafi edici işlemler" (compensating transactions) veya hata kuyrukları gibi stratejiler düşünülmelidir.
- Fail-Closed Prensibi: Güvenlik kontrollerini (yetkilendirme, doğrulama vb.) içeren hata yönetimi, varsayılan olarak erişimi reddetmeli ("fail-closed"), asla akışın devam etmesine izin vermemelidir ("fail-open").
Şimdiye kadar iş mantığının adımlarını, zamanlamasını ve hata durumlarını inceledik. Bir sonraki dersimizde, bu işlemlerin içindeki "sayıların" kendilerine odaklanacağız. "Finansal ve miktar hesaplamalarında sayısal taşma, alt taşma ve kayan nokta hassasiyet hatalarını bulmak" konusunu ele alacağız. Bir finansal işlemin mantığı kusursuz olsa bile, kullanılan veri tiplerindeki küçük bir matematiksel hata veya hassasiyet kaybının nasıl büyük finansal kayıplara veya güvenlik açıklarına yol açabileceğini göreceğiz. Bu konu, matematik altyapınızla da doğrudan rezonans kuracaktır.