GraphQL Introspection Güvenliği: Production Ortamında Nasıl Kapatılır?
Production ortamında bir GraphQL API çalıştırıyorsunuz ve her şey güzel görünüyor. Ancak bir gün güvenlik ekibiniz size şu soruyu soruyor: “API’nin tüm şemasını dışarıdan görebiliyoruz, bu normal mi?” İşte bu an, GraphQL introspection güvenliğini ciddiye almanız gereken andır. Introspection, GraphQL’in en güçlü özelliklerinden biri ama aynı zamanda production ortamında açık bırakıldığında saldırganlar için bir harita niteliği taşıyor.
GraphQL Introspection Nedir ve Neden Tehlikelidir?
GraphQL introspection, bir API’nin kendi şemasını sorgulamanıza olanak tanıyan built-in bir mekanizmadır. Geliştirme aşamasında bu özellik son derece değerlidir; hangi tipler mevcut, hangi alanlar sorgulanabilir, hangi mutasyonlar tanımlı gibi soruların cevabını anında alabilirsiniz. GraphiQL ve Apollo Studio gibi araçlar bu özelliği kullanarak otomatik tamamlama ve dokümantasyon sunar.
Ancak production ortamında introspection açık kaldığında, saldırganlar için şu kapılar aralanır:
- Şema keşfi: Tüm tipler, alanlar, argümanlar ve ilişkiler görünür hale gelir
- Gizli endpoint tespiti: Yalnızca iç kullanıma yönelik mutasyonlar veya sorgular ifşa olur
- Hassas alan adları:
password,token,adminKeygibi alanlar saldırganın dikkatini çeker - İş mantığı sızıntısı: API tasarımından uygulamanın iç yapısı hakkında çıkarım yapılabilir
- Otomatik saldırı kolaylaşması: Burp Suite gibi araçlar introspection sonuçlarını kullanarak otomatik payload üretir
Gerçek dünya örneği olarak şunu düşünün: Bir e-ticaret platformu çalıştırıyorsunuz. Introspection açık olduğunda saldırgan adminCreateDiscount, bypassPaymentVerification veya internalUserMigrate gibi mutation isimlerini görebilir. Bu isimlerin varlığı bile saldırı vektörü oluşturur.
Introspection’ı Tespit Etme
Önce mevcut durumu kontrol edelim. Aşağıdaki sorgu ile API’nizin introspection’a açık olup olmadığını test edebilirsiniz:
curl -X POST https://api.siteniz.com/graphql
-H "Content-Type: application/json"
-d '{"query": "{ __schema { types { name } } }"}'
Eğer bu sorgu yüzlerce tip adı içeren bir JSON döndürüyorsa, introspection aktiftir. Daha kapsamlı bir test için:
curl -X POST https://api.siteniz.com/graphql
-H "Content-Type: application/json"
-d '{
"query": "query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description locations args { ...InputValue } } } } fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } }"
}'
Bu tam introspection sorgusudur ve araçların şemayı keşfetmek için kullandığı standarttır. Eğer bu sorgu çalışıyorsa, derhal harekete geçmeniz gerekiyor.
Node.js / Apollo Server’da Introspection Kapatma
Apollo Server kullanıyorsanız introspection’ı kapatmak oldukça basittir:
# Apollo Server v4 için temel konfigürasyon
# src/server.ts veya server.js dosyanızda
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const server = new ApolloServer({
typeDefs,
resolvers,
// Production ortamında introspection'ı kapat
introspection: process.env.NODE_ENV !== 'production',
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`Server ready at: ${url}`);
Ancak bu yeterli değil. Ortam değişkenini doğru set ettiğinizden emin olun:
# .env.production dosyası
NODE_ENV=production
GRAPHQL_INTROSPECTION=false
# Uygulamayı başlatırken
NODE_ENV=production node dist/server.js
# PM2 ile başlatıyorsanız
pm2 start ecosystem.config.js --env production
PM2 ecosystem dosyası:
// ecosystem.config.js
module.exports = {
apps: [
{
name: 'graphql-api',
script: 'dist/server.js',
instances: 'max',
exec_mode: 'cluster',
env_production: {
NODE_ENV: 'production',
GRAPHQL_INTROSPECTION: 'false',
PORT: 4000,
},
env_development: {
NODE_ENV: 'development',
GRAPHQL_INTROSPECTION: 'true',
PORT: 4000,
},
},
],
};
Rol Tabanlı Introspection Kontrolü
Introspection’ı tamamen kapatmak yerine, yalnızca yetkili kullanıcılara açık tutmak isteyebilirsiniz. Örneğin iç geliştirici ekibiniz production’da şemayı görebilmeli ama dışarıdan kimse görmemeli. Bunun için custom bir yaklaşım gerekir:
import { ApolloServer } from '@apollo/server';
import { ApolloServerPluginInlineTrace } from '@apollo/server/plugin/inlineTrace';
const server = new ApolloServer({
typeDefs,
resolvers,
// Introspection'ı açık tut ama plugin ile kontrol et
introspection: true,
plugins: [
{
async requestDidStart({ request, contextValue }) {
return {
async didResolveOperation({ request, document }) {
// Introspection sorgusu mu kontrol et
const isIntrospection = request.query?.includes('__schema') ||
request.query?.includes('__type');
if (isIntrospection) {
const user = contextValue.user;
const allowedRoles = ['ADMIN', 'DEVELOPER'];
if (!user || !allowedRoles.includes(user.role)) {
throw new Error('Introspection bu ortamda devre dışı bırakılmıştır.');
}
}
},
};
},
},
],
});
Bu yaklaşım production’da çalışırken iç ekibinizin debug ve geliştirme ihtiyaçlarını da karşılar.
Python / Strawberry GraphQL’de Introspection Kapatma
Python ekosisteminde Strawberry kullanıyorsanız:
# FastAPI + Strawberry kombinasyonu için
pip install strawberry-graphql fastapi uvicorn
import strawberry
from strawberry.fastapi import GraphQLRouter
from fastapi import FastAPI
import os
@strawberry.type
class Query:
@strawberry.field
def hello(self) -> str:
return "Merhaba!"
schema = strawberry.Schema(query=Query)
# Production kontrolü
is_production = os.getenv("ENV", "development") == "production"
graphql_app = GraphQLRouter(
schema,
# Production'da introspection'ı kapat
allow_queries_via_get=not is_production,
)
# Introspection için özel extension
from strawberry.extensions import DisableValidationExtension
if is_production:
schema = strawberry.Schema(
query=Query,
extensions=[
# Custom introspection blocker
]
)
app = FastAPI()
app.include_router(graphql_app, prefix="/graphql")
Strawberry için custom introspection blocker:
from strawberry.extensions import SchemaExtension
from strawberry.types import ExecutionContext
import os
class IntrospectionBlocker(SchemaExtension):
def on_executing_start(self):
execution_context: ExecutionContext = self.execution_context
# Introspection sorgularını tespit et
if execution_context.query and (
"__schema" in execution_context.query or
"__type" in execution_context.query
):
env = os.getenv("ENV", "development")
if env == "production":
# Kullanıcı rolünü kontrol et
user = execution_context.context.get("user")
if not user or user.get("role") not in ["admin", "developer"]:
from strawberry.types import ExecutionResult
execution_context.result = ExecutionResult(
data=None,
errors=[
GraphQLError("Introspection production ortamında devre dışıdır.")
]
)
# Schema tanımında kullan
schema = strawberry.Schema(
query=Query,
extensions=[IntrospectionBlocker]
)
Nginx Seviyesinde Introspection Engelleme
Uygulama katmanında yapılan kontrollere ek olarak, Nginx seviyesinde de bir koruma katmanı eklemek iyi bir savunma derinliği pratiğidir. Bu sayede kötü niyetli sorgular uygulamaya hiç ulaşmaz:
# /etc/nginx/sites-available/graphql-api
server {
listen 443 ssl http2;
server_name api.siteniz.com;
# SSL konfigürasyonu
ssl_certificate /etc/letsencrypt/live/api.siteniz.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.siteniz.com/privkey.pem;
location /graphql {
# Request body'yi oku
lua_need_request_body on;
# Introspection keyword'lerini kontrol et
# Bu yaklaşım için nginx-lua modülü gereklidir
access_by_lua_block {
local body = ngx.req.get_body_data()
if body then
if string.find(body, "__schema") or string.find(body, "__type") then
-- İç IP'lerden gelen isteklere izin ver
local remote_addr = ngx.var.remote_addr
if not string.match(remote_addr, "^10%.") and
not string.match(remote_addr, "^192%.168%.") then
ngx.status = 403
ngx.say('{"errors":[{"message":"Introspection devre disi"}]}')
return ngx.exit(403)
end
end
end
}
proxy_pass http://localhost:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_cache_bypass $http_upgrade;
}
}
Lua modülü olmayan standart Nginx için daha basit bir yaklaşım:
# Nginx map kullanarak basit blok
# /etc/nginx/conf.d/graphql-security.conf
map $request_body $introspection_blocked {
default 0;
"~*__schema" 1;
"~*__type" 1;
}
server {
location /graphql {
if ($introspection_blocked) {
return 403 '{"errors":[{"message":"Introspection bu ortamda devre disidir"}]}';
}
proxy_pass http://graphql_backend;
}
}
Nginx konfigürasyonunu test et ve yeniden yükle:
# Konfigürasyonu test et
nginx -t
# Sorun yoksa yeniden yükle
systemctl reload nginx
# Logları takip et
tail -f /var/log/nginx/access.log | grep graphql
Docker ve Kubernetes Ortamında Güvenli Konfigürasyon
Container tabanlı deployment kullanıyorsanız introspection ayarlarını environment variable olarak yönetin:
# docker-compose.yml
version: '3.8'
services:
graphql-api:
image: siteniz/graphql-api:latest
environment:
- NODE_ENV=production
- GRAPHQL_INTROSPECTION=false
- GRAPHQL_PLAYGROUND=false
ports:
- "4000:4000"
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4000/health"]
interval: 30s
timeout: 10s
retries: 3
nginx:
image: nginx:alpine
volumes:
- ./nginx/graphql.conf:/etc/nginx/conf.d/default.conf
ports:
- "443:443"
- "80:80"
depends_on:
- graphql-api
Kubernetes ortamında ConfigMap ve Secret kullanımı:
# graphql-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: graphql-config
namespace: production
data:
NODE_ENV: "production"
GRAPHQL_INTROSPECTION: "false"
GRAPHQL_PLAYGROUND: "false"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: graphql-api
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: graphql-api
template:
spec:
containers:
- name: graphql-api
image: siteniz/graphql-api:1.0.0
envFrom:
- configMapRef:
name: graphql-config
ports:
- containerPort: 4000
Introspection Kapatıldıktan Sonra Yapılması Gerekenler
Introspection’ı kapattınız, ancak bu tek başına yeterli değildir. Beraberinde şu önlemleri de almalısınız:
- Query depth limiting: Saldırganlar derin nested sorgularla DoS saldırısı yapabilir.
graphql-depth-limitgibi kütüphaneler kullanın - Query complexity limiting: Karmaşık sorgular hesaplama maliyetini artırır. Kompleksite skoru belirleyip sınır koyun
- Rate limiting: IP başına sorgu sayısını sınırlayın, özellikle authentication endpointleri için
- Field-level authorization: Her resolver’da yetki kontrolü yapın, şema düzeyinde güvenmek yetmez
- Query whitelisting: Persisted queries kullanarak yalnızca önceden tanımlanmış sorguları kabul edin
Persisted queries ile güvenlik katmanı ekleme örneği:
import { ApolloServer } from '@apollo/server';
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';
// Server tarafında persisted query plugin
import responseCachePlugin from '@apollo/server-plugin-response-cache';
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: false,
plugins: [
{
async requestDidStart() {
return {
async didResolveOperation({ request }) {
// Yalnızca persisted query'lere izin ver
if (process.env.NODE_ENV === 'production' && !request.extensions?.persistedQuery) {
throw new Error('Production ortamında yalnızca persisted query desteklenmektedir.');
}
},
};
},
},
],
});
Monitoring ve Alerting Kurulumu
Introspection kapatıldıktan sonra, biri bu özelliği tekrar aktifleştirirse veya bir şekilde bypass ederse haberdar olmanız gerekir. Bunun için loglama ve alerting kurulumu şarttır:
# GraphQL sorgularını loglayan middleware örneği
# Herhangi bir introspection girişimini kaydet
cat << 'EOF' > /opt/graphql-monitor/check-introspection.sh
#!/bin/bash
# Son 5 dakikadaki logları kontrol et
LOG_FILE="/var/log/nginx/graphql-access.log"
ALERT_EMAIL="[email protected]"
# __schema veya __type içeren istekleri ara
INTROSPECTION_ATTEMPTS=$(grep -c "__schema|__type" "$LOG_FILE" 2>/dev/null || echo "0")
if [ "$INTROSPECTION_ATTEMPTS" -gt "0" ]; then
echo "UYARI: Son log dosyasinda $INTROSPECTION_ATTEMPTS introspection girişimi tespit edildi" |
mail -s "[GUVENLIK] GraphQL Introspection Girişimi" "$ALERT_EMAIL"
fi
EOF
chmod +x /opt/graphql-monitor/check-introspection.sh
# Cron job ekle - her 5 dakikada bir kontrol
echo "*/5 * * * * root /opt/graphql-monitor/check-introspection.sh" >> /etc/cron.d/graphql-security
Geliştirme Ortamı ile Production Arasındaki Denge
Introspection’ı kapatınca geliştirici deneyimi olumsuz etkilenebilir. Bu dengeyi sağlamak için şu yaklaşımı öneriyorum:
- Local development: Introspection tamamen açık, playground aktif
- Staging ortamı: Introspection yalnızca VPN üzerinden erişilebilir, internal IP whitelist var
- Production: Introspection tamamen kapalı ya da yalnızca admin rolüne sahip authenticated kullanıcılara açık
Şema dokümantasyonu için introspection’a bağımlı kalmak yerine graphql-markdown veya spectaql gibi araçlarla statik dokümantasyon üretin. Bu dosyaları iç wiki’nize veya Confluence’a yükleyin. Böylece geliştiriciler şemayı görebilir ama production API’si güvende kalır.
Sonuç
GraphQL introspection, geliştirme sürecinin vazgeçilmez bir parçasıdır ancak production ortamında açık bırakmak, saldırganlara API’nizin tam haritasını vermekle eşdeğerdir. Bu yazıda ele aldığımız önlemler birbirini tamamlayan katmanlar oluşturur: uygulama seviyesinde kapatma, Nginx seviyesinde filtreleme, rol tabanlı erişim kontrolü ve monitoring.
Asıl mesaj şu: Tek bir önlem yeterli değildir. “Defense in depth” prensibini uygulayın. Introspection kapalı olsa bile query complexity, depth limiting ve rate limiting olmadan API’niz hala savunmasız olabilir. Güvenlik bir ürün değil, süreçtir; bu kontrolleri düzenli aralıklarla test edin, ekibinizle gözden geçirin ve pentest raporlarınıza mutlaka dahil edin.
Production’a her deploy öncesi basit bir curl testi yaparak introspection’ın kapalı olduğunu doğrulamayı CI/CD pipeline’ınıza ekleyin. Beş dakikalık bir kontrol, aylarca sürebilecek bir veri ihlalinin önüne geçebilir.
