İçeriğe atla
0 XP 0
12 dkderinleşmeyle 13 dk55 XP6 etkinlik

GraphQL riskleri ve güvenli API tasarımı

Bu derste
  • GraphQL istemciye istediği alanı tek uçtan seçtirir; iç gözlem, derin sorgu ve toplu istek bu esnekliği bir saldırı yüzeyine çevirebilir.
  • Şemanın iç gözlemle nasıl çıkarıldığını, takma adların hız sınırını nasıl aştığını ve sorgu maliyetinin nasıl hesaplandığını göreceksin.
  • İç gözlemi daraltacak, derinlik ve maliyet sınırı koyacak, servisler arası çağrıyı dar kapsamlı token'la bağlayacaksın.

Puan bütün etkinlikleri cevaplayınca hesaba yazılır. Bu dersi tamamlayınca +55 XP alacaksın. Giriş yapmadan ilerlersen puan yalnız bu tarayıcıda durur.

Altay Kargo'nun yeni mobil sürümü REST yerine GraphQL kullanır. Eskiden sipariş ekranı üç ayrı uca istek atıyordu: sipariş, kurye ve teslimat noktası.

Şimdi tek bir sorgu üçünü birden ister ve yalnız gereken alanları alır. Ekip bu esneklikten memnundur, çünkü yeni bir ekran için yeni bir uç yazmak gerekmez. Güvenlik ekibi ise başka bir şey görür.

Tek bir uç, istemcinin yazdığı her sorguyu çalıştırmaya hazırdır. Hangi alanın sorulacağına, sorgunun ne kadar derine ineceğine ve tek istekte kaç işlem yapılacağına artık istemci karar verir. REST'te uç sayısıyla ölçülen saldırı yüzeyi, GraphQL'de sorgunun şekliyle ölçülür.

GraphQL nasıl çalışır, kim çağırır

GraphQL'de istekler genellikle tek bir adrese, örneğin /graphql'e POST olarak gider. Sunucu bir şema taşır. Şema tipleri, alanları ve tipler arası ilişkileri tanımlar.

İstemci bir sorgu yazar ve şemadaki alanlardan istediklerini seçer. Her alanın arkasında bir çözücü (resolver) fonksiyonu çalışır ve veriyi bir veritabanından ya da başka bir servisten getirir. Okuma işlemlerine sorgu (query), yazma işlemlerine mutasyon (mutation) denir.

Yanıt, sorgunun şeklini birebir izleyen bir JSON'dur. HTTP durum kodu çoğu zaman 200'dür; hata olsa bile ayrıntı yanıtın errors alanında gelir. Yalnız durum koduna bakan bir izleme aracı bu yüzden GraphQL hatalarını kaçırır.

query {
  siparis(id: "55087") {
    durum
    kurye { ad telefon }
    teslimatNoktasi { adres }
  }
}
Sipariş ekranının tek sorgusu. Her alanın arkasında ayrı bir çözücü çalışır.

Sorguyu kim gönderirse göndersin, yetki kararı çözücüde verilir. Mobil uygulama kullanıcının token'ıyla gelir. Altay Kargo'nun iç servisleri de aynı GraphQL katmanını kullanır; fatura servisi sipariş bilgisini buradan çeker. Bu servisler kullanıcı adına değil kendi adlarına çağrı yapar.

Token'larını OAuth 2.0'ın istemci kimliği akışıyla, yetkilendirme sunucusunun token uç noktasından alırlar. Kullanıcı arayüzü ve yetki kodu bu akışta hiç devreye girmez. GraphQL'e özgü bir tuzak da burada başlar.

Yetki denetimi yalnız en üst sorguda yapılırsa iç içe alanlar denetimsiz kalabilir. siparis sorgusu sahipliği denetler, ama kurye alanının çözücüsü kuryenin telefonunu herkese dönebilir. Her çözücü kendi verisinin yetkisini sormalıdır.

Etkinlik 1 / 6

İki iç servis birbirine GraphQL üzerinden bağlanıyor. Servisin kimliğini kanıtlayan token'ı aldığı adım hangisidir?

Diyagramda doğru kutuyu tıkla.

İstemci uygulamayönlendirirYetkilendirme sunucusugiriş + onayYetki koduredirect_uriToken uç noktasıkod + PKCEKaynak sunucusuerişim tokenı

İç gözlem: şemanın haritası

GraphQL şemasını sorgulanabilir kılar. Bu özelliğe iç gözlem (introspection) denir ve geliştirici araçları otomatik tamamlamayı bununla yapar. __schema ve __type gibi özel alanlar şemadaki tipleri, alanları, argümanları ve açıklamaları döner. Çoğu GraphQL kütüphanesinde iç gözlem varsayılan olarak açıktır.

Üretimde açık kalırsa saldırgan elle keşif yapmadan veri modelinin tamamını çıkarır. Yönetici mutasyonları, iç alanlar ve belgede hiç geçmeyen tipler bu haritada görünür. REST'te gölge bir ucu bulmak için yolları tahmin etmek gerekir. GraphQL'de iç gözlem açıksa tahmine gerek kalmaz ve keşif tek bir istekle biter.

POST /graphql HTTP/1.1
Host: api.altaykargo.example
Content-Type: application/json

{ "query": "{ __schema { mutationType { fields { name } } } }" }

HTTP/1.1 200 OK
{ "data": { "__schema": { "mutationType": { "fields": [
  { "name": "siparisOlustur" },
  { "name": "adresGuncelle" },
  { "name": "iadeOnayla" },
  { "name": "kullaniciRolDegistir" }
] } } } }
Kimlik doğrulamasız bir iç gözlem isteği, mobil uygulamanın hiç kullanmadığı yönetici mutasyonunu da listeliyor.

İsteği satır satır oku. Gövde tek bir sorgu taşır ve yalnız mutasyon adlarını ister. İstekte Authorization başlığı yoktur, sunucu yine de yanıt verir. Yanıttaki ilk üç mutasyon mobil uygulamanın kullandıklarıdır.

Dördüncüsü, kullaniciRolDegistir, yalnız yönetim panelinde kullanılır ve hiçbir belgede geçmez. Saldırgan artık neyi deneyeceğini bilir. Bu mutasyonun çözücüsünde rol denetimi eksikse fonksiyon düzeyi yetki hatası GraphQL'de yeniden doğar. İç gözlemi kapatmak da tek başına yetmez.

Bazı sunucular yanlış yazılmış bir alan adına "Did you mean" önerisiyle yanıt verir. Bu öneriler iç gözlem kapalıyken bile alan adlarını tek tek sızdırır; üretimde onları da kapat. İç gözlem sorgusunu yalnız kendi lab ortamında ya da yazılı iznin olan bir hedefte dene.

Keşif denemesi kayıtta da görünür. İç gözlem sorgusu gövdede __schema ya da __type kelimesini taşır. Gövdeyi kaydeden bir ağ geçidi bu kelimeleri arayan basit bir kural çalıştırabilir.

Öneri mesajlarından yararlanan araçlar ise başka bir iz bırakır: aynı istemciden kısa sürede yüzlerce geçersiz alan adı gelir ve yanıtların hepsi doğrulama hatası taşır. Normal bir mobil uygulama şemayla uyumlu sorgular gönderir ve neredeyse hiç doğrulama hatası üretmez. Kısa sürede biriken doğrulama hataları bu yüzden keşif işaretidir.

OWASP API8 maddesinde güvenlik yapılandırma hatasını anlatır. Gereksiz yere açık bırakılan özellikler ve hassas bilgi taşıyan hata mesajları bu maddeye girer. Üretimde açık kalan iç gözlem ve alan önerileri de bu tanıma uyar. Ayrıntılı hata mesajlarını da üretimde maskele.

Etkinlik 2 / 6

Bir GraphQL sunucusu üretimde iç gözlemi açık bırakmış. Bu durumun en doğrudan sonucu nedir?

Çözümlü örnek: sorguyu çalıştırmadan önce sınırla

İlk kural iç gözlemi üretimde kapatmaktır. GraphQL'in resmî belgesi bunu yalnız kendi istemcilerine hizmet eden API'ler için önerir. Ama iç gözlemi tamamen kapatmak her zaman mümkün değildir, çünkü bazı iç geliştirici araçları şemayı görmeden çalışamaz. Bu durumda özellik yalnız kimliği doğrulanmış iç araçlara açık tutulur, dış istemcinin aynı sorgusu reddedilir.

Belge bir uyarı da ekler: iç gözlemi kapatmak bir gizleme önlemidir ve API'yi tek başına gizlemeye yetmez. Alan önerileri ve deneme yanılma şemanın şeklini yine ortaya çıkarabilir. Bu yüzden her çözücüde yetki denetimi yine yapılır. Aşağıdaki örnekte Altay Kargo'nun Node.js üstünde çalışan GraphQL katmanı, gelen sorguyu çalıştırmadan önce kurallardan geçirir.

import { parse, validate, specifiedRules, NoSchemaIntrospectionCustomRule } from "graphql";
import depthLimit from "graphql-depth-limit";

const kurallar = [...specifiedRules, depthLimit(6)];
if (process.env.NODE_ENV === "production") kurallar.push(NoSchemaIntrospectionCustomRule);

const belge = parse(govde.query);
const hatalar = validate(schema, belge, kurallar);
if (hatalar.length) return yanitla(400, { errors: hatalar.map((h) => h.message) });
// Yalnız doğrulamadan geçen sorgu çalıştırılır
Altay Kargo'nun GraphQL katmanında doğrulama adımı. Kurallar sorgu çalışmadan önce uygulanır.

Kodu satır satır oku. graphql kütüphanesi şartnamenin zorunlu doğrulama kurallarını specifiedRules adıyla verir. Listeye bir derinlik sınırı eklenir ve altı seviyeden derin sorgu reddedilir. Üretimde NoSchemaIntrospectionCustomRule da eklenir; __schema ya da __type isteyen her sorgu geçersiz sayılır.

parse sorguyu söz dizimi ağacına çevirir, validate bu ağacı kurallardan geçirir. Hata varsa sorgu hiç çalışmaz ve hiçbir çözücü tetiklenmez. Sıra önemlidir. Çalışma başladıktan sonra kesilen bir sorgu veritabanında işini çoktan yapmış olabilir.

İç araçlara iç gözlemi açmak için bu kural, kimliği doğrulanmış iç istekte listeye eklenmez. Ağ geçidi dışarıdan gelen isteklerdeki iç araç işaretini siler, bu yüzden işaret yalnız iç ağdan gelen doğrulanmış istekte anlam taşır.

Etkinlik 3 / 6

İç gözlem sorgusunu yalnız kimlik doğrulamalı bir iç araçtan gönderilecek biçimde işaretleyen başlığı ekle.

İsteği tamamla ve gönder.

POST /graphql HTTP/1.1
Content-Type: application/json
ek başlık

Derin sorgu, takma ad ve toplu istek

GraphQL'de tipler birbirine bağlanır ve bağlar döngü kurabilir. Bir kurye birçok sipariş taşır, her sipariş yine bir kuryeye bağlıdır. İstemci bu döngüyü sorguda sınırsız tekrar edebilir: kurye, siparişleri, her siparişin kuryesi, onun siparişleri. Her seviye bir önceki seviyenin kayıt sayısıyla çarpılır.

Örneğin 50 kurye, her kurye için 20 sipariş ve her sipariş için 10 durum kaydı isteyen bir sorgu 50 × 20 × 10 = 10.000 durum kaydı çözer. Bu hesaba sorgu karmaşıklığı analizi denir. Sunucu her alana bir ağırlık verir ve sorgunun toplam maliyetini çalıştırmadan önce hesaplar. Maliyet bütçeyi aşarsa sorgu reddedilir ya da istemcinin kotasından düşülür.

İkinci risk takma adlar (alias) ve toplu istektir. Takma ad, aynı alanı tek sorguda farklı adlarla birçok kez çağırmaya yarar. Toplu istek ise tek HTTP isteğinde bir işlem dizisi gönderir. İkisi de hız sınırını istek sayısıyla ölçen bir ağ geçidini yanıltır.

Ağ geçidi tek istek görür, sunucu içeride yüzlerce işlem yapar. PortSwigger'ın anlattığı örnekte bir giriş mutasyonu tek istekte farklı parolalarla takma adlar altında tekrarlanır ve deneme sınırı işe yaramaz. OWASP'ın API4 örneğinde küçük resim üreten bir mutasyon tek istekte 999 kez çağrılır ve sunucunun belleği tükenir. Savunma sınırı işlem düzeyine indirir: takma ad sayısı, toplu istekteki işlem sayısı ve hassas mutasyonların istek başına çağrısı ayrı ayrı sınırlanır.

En güçlü katman kalıcı sorgulardır (persisted queries). GraphQL'in belgesi, ekibin yazıp kod incelemesinden geçirdiği kalıcı sorgulara güvenilir belgeler (trusted documents) der. İstemci geliştirme sırasında kullanacağı sorguları sunucuya kaydeder ve her sorgu bir kimlik alır. Çalışma anında istemci sorgunun metnini değil, yalnız kimliğini gönderir.

Sunucu listede olmayan hiçbir sorguyu çalıştırmaz. Saldırgan kendi derin sorgusunu ya da takma ad dizisini yazamaz, yalnız onaylı sorguları çağırabilir. Bu yöntem yalnız kendi istemcilerine hizmet eden API'lere uygundur. Herkesin kendi sorgusunu yazdığı açık bir API'de derinlik, genişlik ve maliyet sınırları birlikte kullanılır.

Derinleş:Otomatik kalıcı sorgu bir güvenlik önlemi değildir

Kalıcı sorgu adı iki farklı özellik için kullanılır ve karışıklık doğurur. Bazı kütüphanelerin sunduğu otomatik kalıcı sorgu (APQ) bir başarım özelliğidir. İstemci önce sorgunun özetini gönderir. Sunucu özeti tanımıyorsa istemci tam sorguyu da yollar ve sunucu onu kaydeder.

Sonraki isteklerde yalnız kısa özet gider ve ağ trafiği azalır. Bu akışta sunucu tanımadığı sorguyu reddetmez, öğrenir. Yani herhangi bir istemci herhangi bir sorguyu kaydettirebilir. Güvenlik sağlayan yöntem ise derleme sırasında oluşturulan kapalı bir listedir.

Bazı platformlar bu yönteme güvenli liste (safelisting) der. Liste yayından önce sunucuya yüklenir ve çalışma anında genişlemez. Yapılandırmada hangisinin açık olduğuna bak. Yalnız özet gönderiliyor diye API'nin kilitli olduğunu varsayma.

Etkinlik 4 / 6

Bu sorgu en kötü durumda kaç teslimat kaydı çözer?

Önce kodu oku, sonra çıktıyı seç.

query {
  subeler(ilk: 10) {
    kuryeler(ilk: 30) {
      teslimatlar(ilk: 40) { durum }
    }
  }
}

Etkinlik 5 / 6

Güvenli bir GraphQL tasarımı hangi önlemleri birlikte kullanır?

Birden fazla doğru var.

Servisler arası çağrı ve eski riskler

GraphQL katmanı çoğu zaman birçok servisin önünde durur. Bir sorgu sipariş, kurye ve fatura servislerine dağılır. Her servise giden çağrı kendi kimliğini ve kapsamını (scope) taşımalıdır. Fatura servisinin token'ı sipariş okuma kapsamı taşır, kullanıcı rolü değiştirme kapsamı taşımaz.

Geniş kapsamlı bir token dar bir görev için bile gereksiz yetki taşır ve ele geçtiğinde bütün kapıları açar. Kapsam, token'ın kim olduğunu değil ne yapabileceğini söyler. Çağrılan servis token'ı doğruladıktan sonra kapsamı istenen işlemle karşılaştırır. Token geçerli ama kapsam uymuyorsa istek reddedilir.

GraphQL, REST'teki risklerin hiçbirini ortadan kaldırmaz, yalnız yerlerini değiştirir. Çözücüye gelen argümanlar kullanıcı girdisidir. Bir argüman SQL sorgusuna doğrudan eklenirse enjeksiyon aynen çalışır.

siparis(id:) gibi bir argüman sahiplik denetimi olmadan kayıt çekerse nesne düzeyi yetki hatası aynen doğar. Asıl fark izleme tarafındadır. Bütün istekler aynı adrese gider ve çoğu 200 döner.

Kayıt tutarken adres ve durum kodu bu yüzden yetmez. İşlem adı, sorgu derinliği, takma ad sayısı ve hesaplanan maliyet de yazılmalıdır. Tarayıcıdan çağrılan bir GraphQL ucu yalnız application/json içerik tipini kabul etmeli, form ve GET ile gelen mutasyonu reddetmelidir; aksi halde siteler arası istek sahteciliğine açık kalır.

Etkinlik 6 / 6

Doğru mu, yanlış mı?

Servisler arası bir çağrıda geçerli bir erişim tokenı, çağıran servisin her işlemi yapabileceği anlamına gelir.

Bu derste bir hata mı gördün? Bildir