Write-upweb-application

GraphQL Güvenliği: Introspection, Batching ve Yetkilendirme

Esnek sorgu dilinin gizli saldırı yüzeyi ve katmanlı savunma stratejisi

AL
AltaySecAltaySec Red Team
26 Ağustos 2026
10 dakika okuma
Özet

GraphQL, istemciye sorgu esnekliği verirken introspection ile şema ifşası, derin/döngüsel sorgularla DoS, alias/batching ile rate-limit atlatma ve alan düzeyi yetkilendirme boşlukları (BOLA/IDOR) gibi REST'te görmediğimiz saldırı yüzeyleri açar. Savunma tek bir ayarla değil; introspection kısıtlaması, sorgu derinliği/karmaşıklık limiti, çözücü seviyesinde yetkilendirme ve persisted queries gibi katmanların birlikte kurulmasıyla sağlanır. Bu yazı her zafiyeti somut sorgu örnekleriyle gösterir ve doğrulanmış CVE'lerle savunma reçetesini verir.

GraphQL neden farklı bir saldırı yüzeyi açıyor?

GraphQL güvenliğinin özü tek cümleyle şudur: istemciye “hangi veriyi, hangi derinlikte, kaç kez ve hangi birleşimle istediğini” belirleme gücü verdiğiniz anda, klasik REST savunmalarının (uç-nokta bazlı rate limit, sabit response şeması, endpoint bazlı yetkilendirme) çoğu tek başına yetersiz kalır. Tek bir /graphql uç noktası; okunabilir bir şema, iç içe ilişkiler ve tek istekte çoklu işlem yürütme yeteneğiyle beraber gelir. Bu esneklik dört ana risk sınıfı yaratır: şema ifşası (introspection), kaynak tüketimi/DoS (derin ve döngüsel sorgular), rate-limit atlatma (alias ve batching) ve alan düzeyi yetkilendirme boşlukları (BOLA/IDOR). Üstüne, çözücülerin (resolver) arkasındaki veri katmanı hâlâ klasik injection açıklarına açıktır. Aşağıda her sınıfı sömürü örnekleriyle açar, sonunda katmanlı savunma reçetesini veririm.

Önemli bir çerçeve düzeltmesi: GraphQL “güvensiz” bir teknoloji değildir. Riskler neredeyse tamamen varsayılan konfigürasyonların üretime taşınmasından ve yetkilendirmenin yanlış katmanda yapılmasından kaynaklanır. OWASP GraphQL Cheat Sheet bu yüzden savunmayı tek bir ayar değil, bir kontrol listesi olarak sunar.

Introspection: şemanın tamamını sunan pencere

Introspection, GraphQL’in yerleşik bir yeteneğidir; istemci __schema ve __type meta-alanlarını sorgulayarak sunucudaki tüm tipleri, sorguları, mutasyonları, argümanları ve hatta description açıklamalarını öğrenebilir. Geliştirme sırasında paha biçilmezdir; üretimde açık bırakıldığında ise saldırgana tam bir harita verir. PortSwigger Web Security Academy, introspection’ı saldırganların şemayı ve gizli alanları keşfetmek için başvurduğu başlıca keşif yöntemlerinden biri olarak vurgular.

Basit bir yoklama ile başlanır:

{ __schema { queryType { name } } }

Bu yanıt verirse, tam şema çıkarımı için standart introspection sorgusu çalıştırılır (tüm tipler, alanlar, argümanlar, deprecated alanlar dahil). Elde edilen JSON, get-graphql-schema gibi araçlarla okunabilir bir SDL’ye dönüştürülüp mutasyonlar ve “istenmeyen özel alanlar” (örneğin passwordHash, internalRole, email) tespit edilir.

Introspection kapatıldığında iş bitmez. İki sızıntı yolu daha vardır:

  • “Did you mean” önerileri: graphql-js hatalı bir alan adında Did you mean "email"? gibi öneriler döndürür. Clairvoyance gibi araçlar bu önerileri kullanarak introspection kapalıyken bile şemayı büyük ölçüde yeniden inşa eder.
  • Filtre atlatma: Introspection’ı naif bir regex ile engelleyen sistemlerde __schema sonrası boşluk/yeni satır/virgül eklemek (query{__schema\n{queryType{name}}}) veya GET/x-www-form-urlencoded gibi alternatif HTTP metotlarıyla istek atmak filtreyi baypaslayabilir.

Doğru savunma, introspection’ı motor seviyesinde kapatmak ve öneri mekanizmasını da devre dışı bırakmaktır:

// graphql-js / express-graphql
const { NoSchemaIntrospectionCustomRule } = require('graphql');

app.use('/graphql', graphqlHTTP({
  schema,
  graphiql: process.env.NODE_ENV === 'development',
  validationRules: [NoSchemaIntrospectionCustomRule],
}));
// graphql-java
GraphQLSchema schema = GraphQLSchema.newSchema()
    .query(queryType)
    .fieldVisibility(NoIntrospectionGraphqlFieldVisibility.NO_INTROSPECTION_FIELD_VISIBILITY)
    .build();

Derin ve döngüsel sorgularla kaynak tüketimi (DoS)

GraphQL şemalarındaki ilişkiler çoğu zaman çift yönlü ve döngüseldir: User → posts → author(User) → posts… veya album → songs → album…. Saldırgan bu döngüyü kötüye kullanarak tek istekle çözücüleri katlanarak çalıştırıp CPU, bellek ve veritabanı bağlantı havuzunu tüketebilir. Payload küçük, sunucu maliyeti devasadır.

query kotu {
  album(id: 42) {
    songs {
      album {
        songs {
          album {
            songs {
              album { id }   # onlarca kat sürdürülebilir
            }
          }
        }
      }
    }
  }
}

Bunun teorik bir risk olmadığını, kütüphanelerin kendi geçmişindeki doğrulanmış zafiyetler gösterir:

  • CVE-2023-28867 (graphql-java): 20.1, 19.4, 18.4 ve 17.5 öncesi sürümlerde, özel hazırlanmış sorgularla yığın (stack) tüketimi yoluyla DoS. Kaynak: Red Hat / NVD kaydı.
  • CVE-2023-26144 (graphql npm / graphql-js): 16.3.0’dan itibaren ve 16.8.1 öncesi sürümlerde, OverlappingFieldsCanBeMergedRule içindeki yetersiz kontroller nedeniyle büyük sorgularla performans düşürme (DoS). Kaynak: GitHub Advisory GHSA-9pv7-vfvm-6vr7.

Bu örnekler kritik dersi verir: motoru güncel tutmak gerekli ama yeterli değildir; derinlik ve karmaşıklık limitlerini uygulama katmanında siz koymalısınız. İki tamamlayıcı kontrol vardır.

1) Derinlik limiti — sorgunun iç içe geçme seviyesini çalıştırmadan önce reddeder:

const depthLimit = require('graphql-depth-limit');

const server = new ApolloServer({
  schema,
  validationRules: [depthLimit(7)], // 7 seviyeden derin sorguları reddet
});

2) Karmaşıklık/maliyet analizi — her alana bir “maliyet” atar; listeler ve iç içe alanlar toplam bütçeyi aşınca sorgu reddedilir. Derinlik limitinin kaçırdığı “geniş ama sığ” sorguları yakalar:

const { createComplexityRule, simpleEstimator } = require('graphql-query-complexity');

const complexityRule = createComplexityRule({
  maximumComplexity: 1000,
  estimators: [simpleEstimator({ defaultComplexity: 1 })],
  onComplete: (c) => console.log('Sorgu karmaşıklığı:', c),
});
// validationRules: [depthLimit(7), complexityRule]

Apollo’nun pratik önerisi: maliyet analizini eklemeden önce staging API’nizi gerçek karmaşık sorgularla çökertmeyi deneyerek eşiği ölçüye dayandırın, tahminle değil.

Batching ve alias ile rate-limit atlatma ve brute-force

Bu, GraphQL’e özgü ve en sık gözden kaçan sınıftır. İki mekanizma vardır:

  • Alias: Aynı alanı tek işlem içinde farklı takma adlarla defalarca çağırmak.
  • Query batching: Tek HTTP isteğinde bir dizi (array) sorgu göndermek.

Çoğu rate limiter HTTP isteği sayısını sayar, işlem sayısını değil. Dolayısıyla tek istekte yüzlerce işlem yürüterek dakikada bir denemeye izin veren korumaları anlamsız hale getirebilirsiniz. PortSwigger’ın brute-force koruması atlatma laboratuvarı tam olarak bunu gösterir: tek istekte alias’lı çok sayıda login denemesi yapıp yanıtta true değerini aramak.

Bir kupon/kod doğrulama uç noktasına karşı alias ile enumerasyon:

query kodDene($c1: Int!, $c2: Int!, $c3: Int!) {
  a1: isValidDiscount(code: $c1) { valid }
  a2: isValidDiscount(code: $c2) { valid }
  a3: isValidDiscount(code: $c3) { valid }
  # ... a1000'e kadar
}

Login brute-force için aynı mantık mutasyonlarla kurulur (kavramsal örnek):

mutation kaba {
  b0: login(input:{user:"admin", password:"123456"}) { token }
  b1: login(input:{user:"admin", password:"password"}) { token }
  b2: login(input:{user:"admin", password:"qwerty"}) { token }
}

Savunma iki koldan gelir. Birincisi, alias ve batching sayısını sınırlamak; ikincisi, hız kısıtlamayı istek sayısına değil işlem/nesne sayısına bağlamak. graphql-js’te aynı alanın alias tekrarını sınırlamak için özel bir validation kuralı yazılabilir; envelop/GraphQL Armor gibi eklentiler bunu hazır sunar:

// GraphQL Armor ile alias ve directive sınırlama (kavramsal konfigürasyon)
import { EnvelopArmor } from '@escape.tech/graphql-armor';

const armor = new EnvelopArmor({
  maxAliases: { n: 15 },      // istek başına alias tavanı
  maxDirectives: { n: 50 },
  maxDepth: { n: 7 },
  costLimit: { maxCost: 1000 },
});

Ek olarak, kimlik doğrulama gibi hassas mutasyonlar için batching’i tamamen kapatmak ve rate limit’i (kullanıcı + IP + hesap bazlı) uygulamak gerekir. OWASP GraphQL Cheat Sheet, kimlik bilgileri, token ve OTP gibi yüksek riskli işlemler için batching’i sınırlamayı veya devre dışı bırakmayı ve nesne düzeyinde hız kısıtlama uygulamayı önerir.

Alan düzeyi yetkilendirme eksikliği: BOLA ve IDOR

GraphQL’in en yıkıcı ve en yaygın gerçek dünya açığı DoS değil, yetkilendirmenin yanlış katmanda yapılmasıdır. Geliştiriciler sık sık yetkiyi tek bir “gateway” veya sorgu girişinde kontrol eder; oysa GraphQL’de saldırgan aynı türü farklı argümanlar ve iç içe ilişkilerle çağırabilir. Yetki her çözücüde ve hem düğüm (node) hem kenar (edge) üzerinde doğrulanmadıkça, bir alandan diğerine “geçiş” mümkün olur.

Klasik IDOR: sıralı ID’lerle başkasının nesnesine erişim.

query { order(id: 1043) { id total customer { email address } } }
# id'yi 1042, 1044... diye gezmek başka kullanıcıların verisini döndürüyorsa BOLA vardır

Daha sinsi olan ilişki üzerinden yetki kaçağıdır: Üst nesneye erişim yetkiniz varken, iç içe bir alan üst nesnenin yetkisini “miras alır” sanılır:

query {
  me {
    projects {           # bunlara erişim yetkim var
      collaborators {    # ama bu kenar başka kullanıcıların
        email            # özel alanlarını sızdırıyorsa yetki kenarda kontrol edilmemiş
        apiKeys { token }
      }
    }
  }
}

Doğru model: yetkilendirmeyi iş mantığı katmanında, her çözücüde yapın; şemaya gömülü “sihirli” yetki varsayımına güvenmeyin. Bağlamdan (context) gelen kimlikle nesne sahipliğini karşılaştırın:

const resolvers = {
  Query: {
    order: async (_, { id }, ctx) => {
      const order = await db.orders.findById(id);
      if (!order) return null;
      // Nesne düzeyinde sahiplik kontrolü — BOLA'yı burada durdurun
      if (order.customerId !== ctx.user.id && !ctx.user.roles.includes('admin')) {
        throw new GraphQLError('Yetkisiz', { extensions: { code: 'FORBIDDEN' } });
      }
      return order;
    },
  },
  Order: {
    // Hassas alanı ayrıca koru — alan düzeyi yetki
    internalNotes: (order, _args, ctx) =>
      ctx.user.roles.includes('admin') ? order.internalNotes : null,
  },
};

Not: Introspection kapalı olsa bile eksik ID’leri deneyerek (product(id: 3) gibi listelenmemiş kaydı çekmek) erişim kontrolü test edilebilir; yani IDOR/BOLA, introspection’dan bağımsız bir açıktır.

Injection: çözücülerin arkasındaki gerçek tehlike

GraphQL’in tip sistemi (scalar, enum, input tipleri) bir miktar giriş doğrulaması sağlar ama injection’ı engellemez. String bir argümanın içeriği hâlâ ham metindir ve çözücü onu SQL, NoSQL, OS komutu veya LDAP sorgusuna güvensiz biçimde geçirirse klasik injection oluşur. Ayrıca argümanların bir URL/host’a geçtiği durumlarda SSRF riski doğar.

# Çözücü bu 'filter' değerini string birleştirmeyle SQL'e koyuyorsa injection
query { users(filter: "'; DROP TABLE users; --") { id } }

Savunma REST ile aynıdır ama disiplin şarttır: parametreli sorgu/prepared statement kullanın, ORM/ODM’yi doğru kullanın, giriş için scalar/enum ve input tipleriyle katı allowlist doğrulaması yapın, kullanıcı girdisini doğrudan dış kaynak/host isteğine koymayın.

Zafiyet x sömürü x savunma haritası

Zafiyet Sömürü tekniği Belirti / gösterge Savunma
Introspection ifşası __schema / __type sorgusu, filtre atlatma, Clairvoyance ile öneri madenciliği Şema, mutasyonlar ve özel alanlar dışarı sızıyor Motor seviyesinde introspection kapat; “Did you mean” önerilerini devre dışı bırak; GraphiQL’i sadece dev’de aç
Derin/döngüsel sorgu DoS İç içe döngüsel sorgu (songs→album→songs…) Küçük payload, yüksek CPU/bellek/DB yükü; CVE-2023-28867, CVE-2023-26144 Derinlik limiti (graphql-depth-limit); karmaşıklık/maliyet analizi; timeout; motoru güncel tut
Alias ile brute-force Tek istekte alias’lı çoklu login/kupon denemesi Rate limit’e rağmen çok sayıda deneme; yanıtta true aranıyor Alias/directive sayısını sınırla; hassas mutasyonlarda batching’i kapat; işlem-bazlı rate limit
Query batching istismarı Tek HTTP isteğinde sorgu dizisi İstek başına yüzlerce işlem Batch boyutunu sınırla veya kapat; nesne düzeyinde hız kısıtlama
BOLA / IDOR Sıralı ID enumerasyonu, ilişki üzerinden yetki kaçağı Başka kullanıcının verisi/özel alanı dönüyor Her çözücüde nesne düzeyi yetki; node + edge kontrolü; alan düzeyi RBAC
Injection / SSRF Argümanın SQL/NoSQL/OS/URL’ye güvensiz geçmesi Hata mesajı sızıntısı, beklenmedik veri/istek Parametreli sorgu; allowlist doğrulama; ORM doğru kullanımı; host girdisini engelle
Aşırı hata ifşası Ham hata/stack trace ile bilgi toplama Yanıtta iç yol, sürüm, alan adı önerileri Üretimde debug: false; hataları maskele, içeride logla

Katmanlı savunma mimarisi

Tek bir kontrol GraphQL’i güvenli kılmaz; kontroller istekten çözücüye giden yolda katman katman dizilmelidir. Öncelik sırasıyla:

  1. Ağ/geçit katmanı: WAF/API gateway, IP ve kullanıcı bazlı rate limit, isteğin gövde boyutu tavanı. x-www-form-urlencoded ve GET ile mutasyon kabul etmeyerek CSRF yüzeyini kapatın.
  2. Doğrulama (validation) katmanı: Derinlik limiti, karmaşıklık/maliyet analizi, alias/directive sayı sınırı, batching sınırı. Bunlar sorgu çalıştırılmadan reddedilmesini sağlar.
  3. Yürütme katmanı: Her çözücüde yetkilendirme (nesne + alan), timeout, veri kaynağına parametreli erişim, DataLoader ile N+1 sömürüsünün maliyetini düşürmek.
  4. Konfigürasyon sertleştirme: Üretimde introspection ve GraphiQL kapalı, debug: false, ayrıntılı hataların maskelenmesi.
  5. Persisted queries (safelisting): En güçlü katman. İstemcinin gönderebileceği sorguları önceden kaydedilmiş bir allowlist‘e kilitlersiniz; istemci ham sorgu yerine bir operasyon ID’si (SHA-256) gönderir. Apollo’nun safelisting dokümanına göre bu, keyfi sorguları tümden reddederek DoS, introspection ve batching istismarlarının çoğunu kökten kapatır.

Kritik ayrım: Automatic Persisted Queries (APQ) bir güvenlik özelliği değildir — Apollo dokümanı, APQ cache’inin gelen her operasyonla güncellendiği için safelisting sağlamadığını belirtir. Güvenlik için, önceden kaydedilmiş ve dışarıdan genişletilemeyen bir persisted query list (PQL) ve tercihen “sadece ID” modu gerekir.

Kapanış: üretime çıkmadan önce doğrulama listesi

GraphQL güvenliği bir “ayar” değil, disiplinli bir mühendislik pratiğidir. Üretime almadan önce şu maddeleri kanıtlayın: introspection ve GraphiQL kapalı mı; derinlik ve karmaşıklık limitleri ölçüye dayalı eşiklerle uygulanıyor mu; alias/batching sınırlı mı; her çözücüde nesne ve alan düzeyinde yetkilendirme var mı; injection’a karşı parametreli erişim kullanılıyor mu; üretimde hata ayrıntıları maskeleniyor mu; mümkünse persisted queries safelist devrede mi. Bu katmanlar birlikte kurulduğunda, GraphQL’in esnekliği bir saldırı yüzeyi olmaktan çıkıp yönetilebilir bir mühendislik yüzeyine dönüşür.


Kaynaklar: OWASP GraphQL Cheat Sheet · PortSwigger — GraphQL API vulnerabilities · CVE-2023-28867 (graphql-java, Red Hat/NVD) · CVE-2023-26144 / GHSA-9pv7-vfvm-6vr7 (graphql-js) · Apollo — Safelisting with Persisted Queries

AL
AltaySec
AltaySec Red Team
AltaySec Arşiv topluluğuna katkı veren yazar. Türkçe siber güvenlik bilgi tabanını birlikte büyütüyoruz.

Sen de yaz, arşivde yerini al

AltaySec Arşiv'e katkı ver — uzmanlığını Türkçe siber güvenlik literatürüne kat.

Yazar Ol