Derin Sorgu Saldırısını Engelleme: GraphQL Query Depth Limiting
GraphQL’in en sevdiğim yanı aynı zamanda en korkutucu yanı: sorgular inanılmaz derecede esnek. Bir client istediği kadar derine inebilir, istediği kadar veri çekebilir. Bu esneklik geliştirme sürecini kolaylaştırırken, kötü niyetli bir kullanıcının elinde sunucunuzu dize getirebilecek bir silaha dönüşebilir. Bugün bu silahın en tehlikeli biçimlerinden birini, yani derin sorgu saldırılarını ve bunlara karşı nasıl önlem alacağımızı konuşacağız.
Derin Sorgu Saldırısı Nedir?
GraphQL’de her şey bir graf üzerinde çalışır. Düğümler birbirine bağlıdır ve siz bu bağlantıları sorguyla keşfedersiniz. Sorun şu ki bu bağlantıların teorik olarak bir sonu yoktur. Bir kullanıcı şöyle bir sorgu yazabilir:
query DerinSorgu {
kullanici {
arkadaslar {
arkadaslar {
arkadaslar {
arkadaslar {
arkadaslar {
id
email
siparisler {
urunler {
kategoriler {
urunler {
id
}
}
}
}
}
}
}
}
}
}
}
Bu sorgu görünürde masum. Ama sunucunuz her katmanda veritabanına yeni sorgular atıyor, her katman bir öncekinin N katı veri döndürüyor. Buna N+1 problemi diyoruz ve derin sorgularla birleştiğinde sisteminizi dakikalar içinde çökertebilir. Saldırgan bunu kasıtlı olarak yapıyorsa buna DoS (Denial of Service) saldırısı diyoruz.
Gerçek dünyada bunu bir e-ticaret sitesinde düşünün. Ürünlerin kategorileri var, kategorilerin alt kategorileri, her kategorinin tekrar ürünleri, her ürünün yorumları, her yorumun sahibi olan kullanıcı, her kullanıcının siparişleri… 10 katman derinliğinde böyle bir sorgu, tek bir HTTP isteğiyle veritabanınızı defalarca turlar.
Query Depth Limiting Nasıl Çalışır?
Çözüm aslında oldukça basit: sorgular belli bir derinliği aşarsa reddet. Query depth limiting, gelen GraphQL sorgusunun ağaç yapısını analiz eder ve maksimum derinliği hesaplar. Bu derinlik belirlediğiniz eşiği geçiyorsa sorgu çalıştırılmadan hata döndürülür.
Derinlik hesaplaması şöyle çalışır: root sorgu 0. seviyedir. Her alan seçimi bir seviye ekler. Fragment’lar da dahil edilerek hesaplama yapılır. Sorgu validation aşamasında, yani resolver’lar çalışmadan önce bu kontrol gerçekleşir.
Node.js ile Pratik Implementasyon
En yaygın GraphQL kütüphanesi olan graphql-js ile başlayalım. Depth limiting için graphql-depth-limit paketini kullanacağız.
npm install graphql-depth-limit
npm install graphql express express-graphql
Basit bir Express + GraphQL kurulumunda depth limiting şöyle eklenir:
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const depthLimit = require('graphql-depth-limit');
const { buildSchema } = require('graphql');
const schema = buildSchema(`
type Kullanici {
id: ID!
ad: String!
arkadaslar: [Kullanici]
siparisler: [Siparis]
}
type Siparis {
id: ID!
urunler: [Urun]
}
type Urun {
id: ID!
ad: String!
kategori: Kategori
}
type Kategori {
id: ID!
ad: String!
urunler: [Urun]
}
type Query {
kullanici(id: ID!): Kullanici
}
`);
const app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true,
validationRules: [depthLimit(5)]
}));
app.listen(4000, () => {
console.log('GraphQL sunucusu 4000 portunda calisiyor');
});
Bu kurulumda 5 derinlik sınırı koyduk. 5 seviyeden derin her sorgu otomatik olarak reddedilir.
Apollo Server ile Depth Limiting
Apollo Server kullanıyorsanız implementasyon biraz farklı ama mantık aynı:
npm install apollo-server graphql-depth-limit
const { ApolloServer, gql } = require('apollo-server');
const depthLimit = require('graphql-depth-limit');
const typeDefs = gql`
type Kullanici {
id: ID!
ad: String!
email: String!
arkadaslar: [Kullanici]
siparisler: [Siparis]
}
type Siparis {
id: ID!
tarih: String!
urunler: [Urun]
kullanici: Kullanici
}
type Urun {
id: ID!
ad: String!
fiyat: Float!
kategori: Kategori
}
type Kategori {
id: ID!
ad: String!
ustKategori: Kategori
urunler: [Urun]
}
type Query {
kullanici(id: ID!): Kullanici
siparisler: [Siparis]
kategoriler: [Kategori]
}
`;
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(7, { ignore: ['IntrospectionQuery'] })
],
formatError: (error) => {
console.error('GraphQL Hatasi:', error.message);
return {
message: error.message,
code: error.extensions?.code || 'UNKNOWN_ERROR'
};
}
});
server.listen().then(({ url }) => {
console.log(`Sunucu hazir: ${url}`);
});
Burada dikkat etmeniz gereken önemli bir şey var: ignore: ['IntrospectionQuery'] parametresi. GraphQL introspection sorguları doğası gereği derindir ve onları da kısıtlarsanız GraphiQL ya da Apollo Studio gibi araçlar çalışmaz. Ama dikkat: production’da introspection’ı tamamen kapatmanız önerilir.
Özel Depth Limiter Yazmak
Bazen paket kullanmak istemezsiniz ya da özel davranışa ihtiyaç duyarsınız. İşte sıfırdan yazılmış basit bir depth limiter:
function createDepthLimiter(maxDepth) {
return function depthLimitValidation(context) {
return {
Field() {
// Mevcut path derinligini hesapla
const depth = context.getAncestors().filter(
ancestor => ancestor.kind === 'Field'
).length;
if (depth > maxDepth) {
context.reportError(
new Error(
`Sorgu derinligi (${depth}) izin verilen maksimum degeri ` +
`(${maxDepth}) asiyor. Sorgunuzu daha az derine inin.`
)
);
}
}
};
};
}
// Kullanim
app.use('/graphql', graphqlHTTP({
schema,
validationRules: [createDepthLimiter(6)]
}));
Bu yaklaşım size tam kontrol sağlar. Örneğin belirli alanlara özel sınırlar koyabilir, loglama ekleyebilir ya da hata mesajını tamamen özelleştirebilirsiniz.
Gerçek Dünya Senaryosu: E-Ticaret API’si
Bir e-ticaret platformu yönettiğinizi düşünün. Ürün kataloğunuz var, kullanıcılar var, siparişler var. Müşterinizin mobil uygulaması bu API’yi kullanıyor. Bir sabah monitörlerinizde CPU spike’ları görmeye başlıyorsunuz. Logları incelediğinizde şunu görüyorsunuz:
# /var/log/graphql/access.log incelemesi
tail -f /var/log/graphql/access.log | grep "POST /graphql"
# Supheceli istekler:
# [2024-01-15 03:42:11] POST /graphql - IP: 185.220.101.x - Duration: 28430ms
# [2024-01-15 03:42:14] POST /graphql - IP: 185.220.101.x - Duration: 31200ms
# [2024-01-15 03:42:17] POST /graphql - IP: 185.220.101.x - Duration: 29800ms
28 saniyeden uzun süren GraphQL sorguları kesinlikle bir şeyler ters olduğunu gösterir. Bu loglardan şüpheli IP’yi yakalayıp sorguyu incelediğinizde 12 katman derinliğinde bir sorgu buluyorsunuz.
Bu noktada yapacaklarınız şunlar olmalı:
- Acil önlem: Nginx veya API Gateway seviyesinde şüpheli IP’yi geçici olarak blokla
- Kısa vadeli: Depth limiting ekle ve mevcut sorguların maksimum derinliğini ölç
- Uzun vadeli: Rate limiting, query complexity analysis ve timeout mekanizmaları kur
Depth limiting’i uyguladıktan sonra loglama ekleyerek durumu izleyebilirsiniz:
const depthLimit = require('graphql-depth-limit');
function depthLimitWithLogging(maxDepth) {
return (context) => ({
Field() {
const ancestors = context.getAncestors();
const depth = ancestors.filter(
a => a.kind === 'Field'
).length;
if (depth === maxDepth - 1) {
// Limite yaklasanlari logla
const ip = context.options?.req?.ip || 'bilinmiyor';
console.warn(
`[UYARI] Derinlik limitine yaklasiliyor. ` +
`IP: ${ip}, Derinlik: ${depth}/${maxDepth}`
);
}
if (depth > maxDepth) {
const ip = context.options?.req?.ip || 'bilinmiyor';
console.error(
`[GUVENLIK] Derinlik limiti asildi! ` +
`IP: ${ip}, Derinlik: ${depth}, Maks: ${maxDepth}`
);
}
}
});
}
Depth Limiting Tek Başına Yeterli Mi?
Hayır, kesinlikle değil. Depth limiting önemli bir katman ama tek başına yeterli olmayan bir güvenlik önlemi. Şunu düşünün: 5 katman derinliğinde ama çok geniş bir sorgu da sisteminizi çökertebilir:
# Derinlik az ama genislik cok olan saldiri
query GenisSorgu {
urun1: urun(id: "1") { id ad }
urun2: urun(id: "2") { id ad }
urun3: urun(id: "3") { id ad }
# ... yuzlerce alan devam ediyor
}
Bu yüzden depth limiting’i şu önlemlerle birlikte kullanın:
- Query Complexity Analysis: Her alan bir maliyet değeri taşır, toplam maliyet belirli bir eşiği geçemez
- Rate Limiting: Belirli zaman diliminde belirli IP’den maksimum X sorgu
- Query Timeout: Resolver’lar belirli süreden uzun çalışamaz
- Persisted Queries: Sadece önceden kayıtlı sorgulara izin ver
- Introspection Kısıtlaması: Production’da introspection kapat
Nginx ile Rate Limiting ve Timeout
GraphQL güvenliği sadece uygulama katmanında değil, altyapı katmanında da başlar. Nginx konfigürasyonunuza şunları ekleyin:
# /etc/nginx/conf.d/graphql.conf
# Rate limiting zone tanimlama
limit_req_zone $binary_remote_addr zone=graphql_limit:10m rate=30r/m;
server {
listen 443 ssl;
server_name api.siteniz.com;
location /graphql {
# Dakikada maksimum 30 istek, 5 istek burst'a izin ver
limit_req zone=graphql_limit burst=5 nodelay;
limit_req_status 429;
# POST disindaki metodlari reddet
limit_except POST OPTIONS {
deny all;
}
# Maksimum request body boyutu (buyuk sorgulari engelle)
client_max_body_size 10k;
# Upstream timeout
proxy_read_timeout 10s;
proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_pass http://localhost:4000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Rate limit asisinda ozel hata mesaji
error_page 429 @rate_limit_error;
}
location @rate_limit_error {
add_header Content-Type application/json;
return 429 '{"errors":[{"message":"Cok fazla istek. Lutfen biraz bekleyin."}]}';
}
}
Doğru Derinlik Sınırı Nasıl Belirlenir?
Bu sorunun cevabı “duruma göre değişir” ama bazı pratik yöntemler var.
Önce mevcut production sorgularınızın derinliklerini ölçün. Bunun için sorgularınızı loglamanız gerekiyor:
# Mevcut sorgularinizin derinligini olcmek icin
# graphql-query-complexity veya benzeri araclari kullanabilirsiniz
const { getComplexity, simpleEstimator } = require('graphql-query-complexity');
app.use('/graphql', (req, res, next) => {
const query = req.body.query;
if (query) {
// Sorgu derinligini logla (depth-limit paketi ile)
const depth = calculateDepth(query);
console.log(`Sorgu derinligi: ${depth}`);
}
next();
});
Genel öneriler şu şekilde:
- Public API: Maksimum 5-7 derinlik genellikle yeterlidir
- Internal API: 10-12 derinliğe kadar çıkılabilir
- Mobil uygulama backend’i: Hangi sorguları kullandığınızı biliyorsanız en derin sorgunuzun 2 üstü güvenli bir sınır
Sınırı belirlerken ekibinizin kullandığı en derin meşru sorguyu bulun, üstüne 2-3 ekleyin ve oradan başlayın. İlk haftada logları izleyin, false positive varsa sınırı biraz gevşetin.
Test Etme
Depth limiting’in doğru çalıştığını doğrulamak için testler yazmalısınız. Jest ile basit bir test seti:
const { graphql } = require('graphql');
const depthLimit = require('graphql-depth-limit');
const { schema } = require('./schema');
describe('Query Depth Limiting', () => {
const MAX_DEPTH = 5;
const validationRules = [depthLimit(MAX_DEPTH)];
test('Izin verilen derinlikte sorgu basarili olmali', async () => {
const query = `
query {
kullanici(id: "1") {
ad
siparisler {
id
urunler {
ad
}
}
}
}
`;
const result = await graphql({
schema,
source: query,
validationRules
});
expect(result.errors).toBeUndefined();
});
test('Maksimum derinligi asan sorgu reddedilmeli', async () => {
const query = `
query {
kullanici(id: "1") {
arkadaslar {
arkadaslar {
arkadaslar {
arkadaslar {
arkadaslar {
id
}
}
}
}
}
}
}
`;
const result = await graphql({
schema,
source: query,
validationRules
});
expect(result.errors).toBeDefined();
expect(result.errors[0].message).toContain('exceeds maximum operation depth');
});
test('Fragment kullanan derin sorgu da reddedilmeli', async () => {
const query = `
fragment KullaniciBilgi on Kullanici {
arkadaslar {
arkadaslar {
arkadaslar {
id
}
}
}
}
query {
kullanici(id: "1") {
...KullaniciBilgi
}
}
`;
const result = await graphql({
schema,
source: query,
validationRules
});
expect(result.errors).toBeDefined();
});
});
Bu testleri CI/CD pipeline’ınıza ekleyin. Her deployment’tan önce güvenlik kurallarının doğru çalıştığından emin olun.
Monitoring ve Alerting
Güvenlik önlemleri sadece koyup unutmak için değildir. Prometheus metrikleriyle depth limit ihlallerini izleyebilirsiniz:
const prometheus = require('prom-client');
const depthViolationCounter = new prometheus.Counter({
name: 'graphql_depth_limit_violations_total',
help: 'Derinlik limiti ihlali sayisi',
labelNames: ['ip', 'endpoint']
});
// Depth limiting middleware'i ile entegrasyon
function instrumentedDepthLimit(maxDepth) {
return (context) => ({
Field() {
const ancestors = context.getAncestors();
const depth = ancestors.filter(a => a.kind === 'Field').length;
if (depth > maxDepth) {
const ip = context.options?.req?.ip || 'unknown';
depthViolationCounter.inc({ ip, endpoint: '/graphql' });
// Slack veya PagerDuty alerti gonder
if (shouldAlert(ip)) {
sendSecurityAlert({
type: 'DEPTH_LIMIT_VIOLATION',
ip,
depth,
maxDepth,
timestamp: new Date().toISOString()
});
}
}
}
});
}
Grafana dashboard’unuzda bu metriği izleyin. Anlık spike’lar saldırı girişimlerini, sürekli yüksek değerler ise belki meşru ama kötü yazılmış bir client’ı gösterir.
Sonuç
GraphQL’in gücü aynı zamanda onun en büyük güvenlik riskidir. Derin sorgu saldırıları gerçek bir tehdit ve production sistemleri etkileyen örnekler her geçen gün artıyor. Query depth limiting bu tehdidin önünde duran ilk ve en temel kalkan.
Bugün anlattıklarımızı özetleyecek olursak:
- Depth limiting’i validation katmanında uygulayın, resolver’lar çalışmadan önce engelleyin
- Başlangıç için genellikle 5-7 derinlik sınırı mantıklıdır, production loglarınızdan gerçek değeri belirleyin
- Depth limiting’i tek başına yeterli görmeyin; rate limiting, timeout ve complexity analysis ile birleştirin
- Altyapı katmanında Nginx ile ikinci bir koruma katmanı ekleyin
- Testleri CI/CD’ye dahil edin, güvenlik önlemleri her deployment’ta doğrulanmalı
- Monitoring ve alerting olmadan güvenlik kördür, ihlalleri izleyin ve alarm kurun
Son olarak şunu söylemek isterim: güvenlik tek seferlik bir iş değil, sürekli bakım gerektiren bir süreç. Bugün koyduğunuz depth limiter yeterli görünebilir, ama saldırganlar yaratıcıdır. Complexity saldırıları, alias bombing, fragment döngüleri… bunların hepsi ayrı yazı konuları. Ama sağlam bir depth limiting altyapısıyla en azından en düşük asılı meyveleri kapıp gitmelerini engellersiniz.
