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, adminKey gibi 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-limit gibi 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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir