Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 20

Çok Kiracılı ORM/Veri Erişimi İzolasyon Açıklarını Bulma

Recommended resources

Merhaba, önceki dersimizde rol hiyerarşilerindeki ve rol atama kodundaki mantık hatalarını analiz ederek yetki yükseltme zafiyetlerini nasıl tespit edeceğimizi inceledik. Bir kullanıcının sistem içinde ne yapabileceğini tanımlayan bu yetkilendirme kontrolleri, güvenlik duvarının ilk katmanını oluşturur.

Bu dersimizde ise, özellikle Hizmet Olarak Yazılım (SaaS) platformlarının temelini oluşturan bir mimari desene odaklanacağız: Çok Kiracılı (Multi-tenant) Mimariler. Konumuz artık bir kullanıcının ne yapabildiğinden çok, hangi veriyi görebildiği olacak. ORM sorguları ve veri erişim katmanlarındaki küçük bir hatanın, bir kiracının (tenant) verilerinin başka bir kiracıya sızmasına nasıl yol açabileceğini kod seviyesinde inceleyeceğiz. Bu, günümüz bulut uygulamalarında en kritik ve en sık rastlanan zafiyet türlerinden biridir.

Çok Kiracılı Mimarinin Temelleri: İzolasyon ve Verimlilik Dengesi

Çok kiracılılık, en basit tanımıyla, tek bir yazılım uygulamasının ve altyapısının birden fazla müşteriye (kiracıya) aynı anda hizmet vermesidir. Her kiracı kendi özel uygulamasına sahipmiş gibi hissederken, perde arkasında aynı sunucuları, aynı kod tabanını ve sıklıkla aynı veritabanını paylaşırlar. Bu yaklaşımın temel motivasyonu verimliliktir: kaynakları havuzlayarak maliyetleri düşürmek ve tek bir platformu yöneterek operasyonel yükü azaltmak.

Ancak bu verimliliğin bir bedeli vardır: izolasyon riski. Paylaşılan kaynaklar, kiracılar arasındaki mantıksal sınırların her zaman ve her yerde kusursuz bir şekilde uygulanmasını gerektirir. Aksi takdirde, "eksik bir filtre, yanlış yapılandırılmış bir izin veya kiracı bağlamı olmadan çalışan bir arka plan işi" gibi sıradan hatalar, felaketle sonuçlanabilecek veri sızıntılarına yol açabilir.

Bu konudaki teorik altyapıyı ve temel desenleri daha iyi anlamak için lütfen aşağıdaki kaynağı inceleyin.

Çok Kiracılı SaaS Desenleri: İzolasyon, Ölçek ve Yapay Zeka ...

Bu makale, çok kiracılı mimarinin ne anlama geldiğini, neden tercih edildiğini ve en temel başarısızlık modlarını (güvenlik ve performans) açıklıyor. Özellikle izolasyon ve verimlilik arasındaki temel dengeyi anlamak için değerli bir çerçeve sunuyor.

Lütfen makalenin başından başlayarak başlangıç bölümünü okuyun. Ardından, "Temel Takas" başlığı altındaki bölümü dikkatlice inceleyin. Bu kısım, bir mimari seçerken güvenlik izolasyonu ile maliyet/operasyon verimliliği arasındaki dengeyi nasıl kurduğumuzu açıklıyor. Bu dengeyi anlamak, kodda neden belirli desenlerin tercih edildiğini veya edilmediğini yorumlamanıza yardımcı olacaktır.


Veri İzolasyon Desenleri ve Riskleri

Çok kiracılı sistemlerde veri izolasyonu "bir müşteri başka bir müşterinin verisini görebilir mi?" sorusuna cevap verir. Bu izolasyonu sağlamak için temelde üç ana desen kullanılır. Her birinin güvenlik ve operasyonel maliyet açısından farklı sonuçları vardır.

  1. Veritabanı Başına Kiracı (Database-per-tenant): Her kiracının kendine ait ayrı bir veritabanı vardır. Bu, en güçlü izolasyonu sağlar çünkü sınırlar fiziksel olarak nettir. Güvenlik denetimi, uygulamanın doğru isteği doğru veritabanı bağlantısına yönlendirip yönlendirmediğini kontrol etmeye odaklanır.

  2. Şema Başına Kiracı (Schema-per-tenant): Tüm kiracılar aynı veritabanı sunucusunu paylaşır, ancak her kiracının verileri kendi özel şeması (örneğin PostgreSQL'de tenant1, tenant2 gibi) içinde tutulur. İzolasyon hâlâ güçlüdür. Veritabanı bağlantısı kurulduktan sonra, sorguların doğru şemaya yönlendirildiğinden emin olunmalıdır.

Aşağıdaki görseller bu yapıyı net bir şekilde göstermektedir. İlk görselde, public şeması tüm kiracılar için ortak olan ve kiracıları yöneten tabloları içerir.

`public` şeması altında yer alan `tenants_tenant` ve `tenants_domain` gibi tablolar, sistemin hangi kiracıların var olduğunu ve onlara nasıl erişileceğini yönetir. Bu, tüm kiracılar için ortak olan merkezi bir yönetim noktasıdır.

İkinci görselde ise tenant1 ve tenant2 için oluşturulmuş ayrı şemaları ve bu şemaların kendi özel tablolarını (polls_question vb.) içerdiğini görüyoruz.

Her kiracı (`tenant1`, `tenant2`) kendi şemasına sahiptir ve uygulama verileri bu şemalar içinde tamamen izole edilmiştir. `tenant1` içindeki bir sorgu, doğal olarak `tenant2` verilerine erişemez.
  1. Satır Düzeyinde İzolasyon (Row-level Isolation): Tüm kiracılar aynı veritabanını, aynı şemayı ve aynı tabloları paylaşır. Hangi satırın hangi kiracıya ait olduğu, her tabloya eklenen tenant_id gibi bir sütun ile belirlenir. Bu, operasyonel olarak en basit ve en düşük maliyetli modeldir, ancak güvenlik açısından en riskli olanıdır. Kod denetiminde en çok odaklanacağımız yer burasıdır, çünkü tek bir sorguda tenant_id filtresinin unutulması, tüm kiracıların verilerinin sızdırılmasına yol açabilir.

Bu üç deseni ve getirdikleri riskleri daha detaylı anlamak için lütfen daha önce başladığınız makalenin ilgili bölümünü okuyun.

Çok Kiracılı SaaS Desenleri: İzolasyon, Ölçek ve Yapay Zeka ...

Bu bölüm, bahsettiğimiz üç ana veri izolasyon desenini (satır, şema, veritabanı) ve hibrit modelleri karşılaştırıyor. Özellikle satır düzeyinde izolasyonun getirdiği risklere ve bu riskleri azaltma yöntemlerine odaklanıyor.

Makalede "Çok Kiracılı Modeller" başlığını atlayarak doğrudan "Veri İzolasyon Desenleri" bölümüne geçin. Bu bölümdeki üç ana deseni (Satır düzeyinde izolasyon, Şema-başına-kiracı, Veritabanı-başına-kiracı) ve her birinin risk/azaltma yöntemlerini dikkatlice okuyun. Bu, kodda ne aramanız gerektiğini anlamanız için kritik öneme sahiptir.


Kodda İzolasyon Başarısızlıklarını Bulma

Teoriyi bir kenara bırakıp pratiğe dönersek, güvenli kod denetimindeki görevimiz, özellikle en riskli model olan satır düzeyinde izolasyondaki hataları bulmaktır. Zafiyet, neredeyse her zaman aynı temel hatadan kaynaklanır: veritabanı sorgusunda tenant_id filtresinin eksik olması.

Tehlikeli Desen: Unutulan Filtre

Python/Django ORM'i üzerinden bir örnek düşünelim. Bir ürün detayını getiren bir API endpoint'i olsun:




# TEHLİKELİ DESEN

def get_product_details(request, product_id):



    # Kullanıcının kimliği doğrulanmış ve hangi kiracıya ait olduğu biliniyor (örn: request.tenant).
    # Ancak sorgu bu bilgiyi kullanmıyor.
    try:



        # ZAFİYET BURADA: Sorgu sadece 'id' üzerinden yapılıyor.
        # Eğer saldırgan başka bir kiracıya ait geçerli bir product_id bilirse,
        # o ürünün detaylarını getirebilir. Bu bir IDOR zafiyetidir.
        product = Product.objects.get(id=product_id) 
        return JsonResponse(product.to_dict())
    except Product.DoesNotExist:
        return JsonResponse({"error": "Product not found"}, status=404)

Güvenli Desen: Zorunlu Filtre

Aynı fonksiyonun güvenli hali, her sorguya mevcut kullanıcının kiracı bağlamını ekler:




# GÜVENLİ DESEN

def get_product_details(request, product_id):
    current_tenant = request.tenant # Middleware tarafından eklenmiş
    try:



        # DÜZELTME: Sorgu hem 'id' hem de 'tenant_id' ile kısıtlanıyor.
        # Artık saldırgan başka bir kiracının product_id'sini bilse bile,
        # sorgu kendi kiracısı içinde eşleşme bulamayacağı için "DoesNotExist" hatası alır.
        product = Product.objects.get(id=product_id, tenant_id=current_tenant.id)
        return JsonResponse(product.to_dict())
    except Product.DoesNotExist:
        return JsonResponse({"error": "Product not found"}, status=404)

Denetim Stratejisi: Nereye Bakmalı?

  1. Her Veri Erişim Noktası: Kod tabanındaki Model.objects.get(), Model.objects.filter(), .first(), .all() gibi tüm ORM çağrılarını inceleyin. Bu çağrılar bir kiracı filtresi içeriyor mu?

  2. Base Modeller ve Yöneticiler (Managers): Yetkin geliştirme ekipleri, bu hatayı önlemek için mimari çözümler üretir. Örneğin, Django'da özel bir "Manager" veya "QuerySet" tanımlayarak tenant_id filtresini otomatik olarak tüm sorgulara ekleyebilirler.

    
    
    
    # Otomatik filtreleme sağlayan bir custom manager örneği
    class TenantScopedManager(models.Manager):
        def get_queryset(self):
    
    
    
            # Bu manager kullanıldığında, tüm sorgulara otomatik olarak
            # tenant filtresi eklenmesi hedeflenir.
            # Ancak bunun nasıl uygulandığını (örn. bir thread-local'den mi tenant'ı alıyor)
            # ve her modelde kullanılıp kullanılmadığını doğrulamak gerekir.
            # ...
    

    Eğer böyle bir yapı varsa, göreviniz iki yönlüdür:

    • Bu otomatik mekanizma gerçekten güvenli mi? (Örn: Kiracı bağlamını nereden alıyor?)
    • Veri çeken tüm modeller bu güvenli base class'ı veya manager'ı kullanıyor mu? İstisnalar var mı? LINK kaynağında Laravel için query scopes ile gösterilen örnek, bu merkezi denetim mantığına iyi bir örnektir.
  3. Ham SQL Sorguları (.raw()): Geliştiriciler performans optimizasyonu veya karmaşık join'ler için ORM'den çıkıp ham SQL yazdıklarında, tüm otomatik güvenlik mekanizmaları devre dışı kalır. Bu sorgular kırmızı bayraktır ve özellikle dikkatle incelenmelidir.

  4. Arka Plan İşleri (Background Jobs): Bir web isteği (request) genellikle bir kiracı bağlamına sahiptir. Peki bu istek tarafından tetiklenen bir asenkron arkaplan işine (örn. Celery task) bu bağlam doğru bir şekilde aktarılıyor mu? Çoğu zaman aktarılmaz ve arkaplan işi "tanrım modunda" çalışarak yanlışlıkla kiracı sınırlarını ihlal edebilir.

  5. Paylaşılan Önbellek (Caching): Önbelleğe alınan veriler için kullanılan anahtarlar kiracıya özgü mü? cache.get(f"product_{product_id}") gibi bir anahtar, A kiracısının ürününü B kiracısının görmesine neden olabilir. Güvenli anahtar cache.get(f"tenant_{tenant_id}:product_{product_id}") gibi olmalıdır.


İleri Seviye Savunma: "Dönüştür, Doğrulama!" (Parse, Don't Validate)

Her geliştiricinin her sorguda tenant_id eklemesini ummak, insan hatasına açık bir stratejidir. Daha sağlam bir mimari yaklaşım, sistemi güvensiz sorgu yazmayı imkansız hale getirecek şekilde tasarlamaktır. Bu, "Parse, Don't Validate" prensibinin veri erişim katmanına uygulanmasıdır.

Bu prensibin arkasındaki temel fikir, daha az yapılandırılmış bir girdiyi (örn. genel bir HTTP isteği) alıp, programın geri kalanında güvenli operasyonları garanti eden daha yapılandırılmış bir çıktıya (örn. TenantScopedContext) dönüştürmektir.

Bu güçlü konsepti daha iyi anlamak için aşağıdaki videoyu izlemenizi öneririm.

Parse, don't validate! / Dönüştür, doğrulama!

"hafif programming" kanalından bu video, "Parse, don't validate" prensibini ve tip sistemini kullanarak hataları derleme zamanında nasıl önleyebileceğimizi harika bir şekilde açıklıyor. Konu doğrudan multi-tenancy olmasa da, sunduğu zihniyet, güvenli veri erişim katmanları tasarlamak için birebirdir.

İlk olarak, prensibin ana fikrini anlamak için arasındaki "less structured to more structured" tanımını izleyin. Ardından, spesifik veri tiplerinin hataları nasıl azalttığını görmek için bölümünü dikkatlice dinleyin. Bizim durumumuzda, Tenant tipini almayan bir veri erişim fonksiyonu yazmayı imkansız kılmaktan bahsediyoruz. Son olarak, konseptin ORM katmanlarıyla olan doğrudan bağlantısını görmek için bölümünü izleyin. Videonun arasındaki tartışma bölümü, bu yaklaşımın sadece fonksiyonel dillerde değil, C# ve Java gibi dillerde de nasıl uygulanabileceğini ve kod kültüründeki yerini tartışıyor. Bu, sizin C# ve Java'daki tecrübenizle de paralellik kurmanıza yardımcı olabilir.

Bu prensibi multi-tenancy'ye uygularsak:

  • Yasakla: Product.objects.get(...) gibi global erişim fonksiyonlarını tamamen yasakla (veya sadece çok kısıtlı süper admin kodunda izin ver).
  • Zorunlu Kıl: Veriye erişmenin tek yolu, bir kiracı bağlam nesnesi üzerinden olsun:
    
    
    
    # Middleware, isteği doğrular ve bir Tenant nesnesi oluşturur.
    tenant = get_tenant_from_request(request)
    
    
    
    
    # Veri erişim katmanı, bu tenant nesnesini parametre olarak alır.
    dal = DataAccessLayer(tenant)
    
    
    
    
    # Artık tüm sorgular bu 'dal' nesnesi üzerinden yapılır ve 'tenant' bağlamı içinde
    # kalması garanti edilir.
    product = dal.get_product(product_id) 
    

Bu tasarımda, bir geliştiricinin tenant_id filtresini "unutması" imkansızdır, çünkü dal nesnesi onsuz çalışmaz. Kod denetiminde böyle bir mimari desen görmek, sistemin veri izolasyonu konusunda olgun bir yaklaşıma sahip olduğunun güçlü bir işaretidir.


Sonuç

Bu dersimizde, modern SaaS uygulamalarının en kritik güvenlik gereksinimlerinden biri olan çok kiracılı veri izolasyonunu kod seviyesinde nasıl denetleyeceğimizi öğrendik.

Temel Çıkarımlar:

  • İzolasyon Modellerini Anlayın: Bir uygulamanın hangi izolasyon modelini (veritabanı, şema, satır) kullandığını belirlemek, riskin nerede yoğunlaştığını anlamanın ilk adımıdır. En büyük risk, satır düzeyinde izolasyondadır.
  • Filtreyi Avlayın: Satır düzeyinde izolasyon kullanan sistemlerdeki denetimin temel amacı, her veri sorgusunun bir tenant_id filtresi içerip içermediğini doğrulamaktır.
  • İstisnalara ve Çevre Birimlere Dikkat Edin: Ham SQL sorguları, arkaplan işleri ve önbellekleme, merkezi güvenlik mekanizmalarının atlandığı yaygın kaçış noktalarıdır.
  • Mimariye Odaklanın: En güvenli sistemler, doğru şeyi yapmayı ummak yerine, yanlış şeyi yapmayı imkansız kılan mimarilere sahiptir. "Parse, don't validate" prensibiyle tasarlanmış bir veri erişim katmanı, bunun en iyi örneğidir.

Önceki derslerde bir kullanıcının kimliğini, yetkilerini ve rollerini inceledik. Bu derste ise o kullanıcının sadece kendi verilerine erişebilmesini sağladık. Bir sonraki modülümüzde, bu temellerin üzerine inşa ederek çok daha karmaşık bir alana gireceğiz: İş Mantığı ve Eşzamanlılık Zafiyetleri. Artık tekil erişim kontrollerinden ziyade, bir kullanıcının gerçekleştirdiği eylemlerin sırasına ve zamanlamasına odaklanacağız. Uygulama iş akışlarını durum makineleri olarak modelleyerek, beklenen işlem sırasının dışına çıkmanın veya zamanlama farklılıklarından yararlanmanın nasıl güvenlik zafiyetleri oluşturabileceğini analiz edeceğiz.