GraphQL API Güvenliği: En Kritik Tehditler ve Çözümler
Modern web uygulamalarında GraphQL’in yükselişi inanılmaz hızda gerçekleşti. REST API’lerin katı yapısından sıkılan geliştiriciler için GraphQL adeta bir kurtuluş oldu. Ancak bu esneklik beraberinde ciddi güvenlik risklerini de getirdi. Bir sysadmin veya backend geliştirici olarak GraphQL endpoint’ini açığa çıkardığında, aslında çok güçlü bir sorgu dili de sunmuş oluyorsun saldırganlara. Bu yazıda production ortamlarında karşılaşılan gerçek tehditlerden ve bunlara karşı uygulanabilir çözümlerden bahsedeceğim.
GraphQL Neden Özel Güvenlik Gereksinimleri Doğurur
REST API’de bir endpoint bir işi yapar. /users kullanıcıları getirir, /orders siparişleri getirir. GraphQL’de ise tek bir endpoint olan /graphql üzerinden her şeyi sorgulayabilirsin. Bu mimari fark, güvenlik yaklaşımını tamamen değiştiriyor.
GraphQL’in doğasında üç temel özellik var ki bunlar yanlış yapılandırıldığında ciddi açıklara dönüşüyor:
- İç gözlem (Introspection): Şemanın tamamını sorgulamak mümkün
- İlişkisel sorgular: Nested objeler aracılığıyla derinlemesine veri çekme
- Toplu işlemler (Batching): Tek istekte birden fazla sorgu çalıştırma
Şimdi her bir tehdit vektörünü inceleyelim.
Tehdit 1: Introspection ile Şema Keşfi
Production ortamında introspection açık bırakmak, binanın planını kapıya asmak gibi bir şey. Saldırgan aşağıdaki sorguyla sistemindeki tüm type’ları, field’ları ve mutation’ları öğrenebilir.
curl -X POST https://api.example.com/graphql
-H "Content-Type: application/json"
-d '{
"query": "{ __schema { types { name fields { name type { name } } } } }"
}'
Bu sorgu çalışırsa saldırgan sisteminin tam haritasını çıkarır. Hangi field’ların hassas veri içerdiğini, hangi mutation’ların mevcut olduğunu, hatta internal type isimlerini görür.
Çözüm: Production’da Introspection’ı Devre Dışı Bırak
Apollo Server kullanıyorsan bu oldukça basit:
# Node.js / Apollo Server konfigürasyonu
# src/server.js dosyasında
const { ApolloServer } = require('@apollo/server');
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production',
plugins: [
{
requestDidStart() {
return {
didResolveOperation({ request, document }) {
const operationName = request.operationName;
if (process.env.NODE_ENV === 'production') {
const hasIntrospection = document.definitions.some(
def => def.selectionSet?.selections?.some(
sel => sel.name?.value?.startsWith('__')
)
);
if (hasIntrospection) {
throw new Error('Introspection is disabled in production');
}
}
}
};
}
}
]
});
Python/Graphene kullanıyorsan middleware ile engelleyebilirsin:
# Python Flask + Graphene örneği
# app.py
from flask import Flask, request, jsonify
from graphene_flask import GraphQLView
import os
class IntrospectionMiddleware:
def resolve(self, next, root, info, **args):
if os.environ.get('ENVIRONMENT') == 'production':
if info.field_name.startswith('__'):
raise Exception('Introspection queries are not allowed')
return next(root, info, **args)
app = Flask(__name__)
app.add_url_rule(
'/graphql',
view_func=GraphQLView.as_view(
'graphql',
schema=schema,
middleware=[IntrospectionMiddleware()]
)
)
Tehdit 2: DoS Saldırıları ve Query Depth Bombing
GraphQL’in nested sorgu yapısı, kötü niyetli bir kullanıcının sistemi çökertmesi için mükemmel bir araç sunuyor. Şu senaryoyu düşün: User tipinin friends field’ı var, friends de yine User döndürüyor. Bir saldırgan şöyle bir sorgu yazabilir:
# Depth bomb örneği - bu tür sorguları ASLA production'a geçirme
query DepthBomb {
user(id: "1") {
friends {
friends {
friends {
friends {
friends {
friends {
id
email
# 50 seviye daha devam eder...
}
}
}
}
}
}
}
}
Bu sorgu veritabanını exponential biçimde sorgular ve sunucuyu kolayca dize getirir.
Çözüm: Query Depth ve Complexity Limitleme
graphql-depth-limit ve graphql-query-complexity paketleri bu konuda standart çözümler sunuyor:
# Paketleri yükle
npm install graphql-depth-limit graphql-query-complexity
# server.js içinde konfigürasyon
const depthLimit = require('graphql-depth-limit');
const { createComplexityLimitRule } = require('graphql-query-complexity');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(7), # Maksimum 7 seviye derinlik
createComplexityLimitRule(1000, {
onCost: (cost) => {
console.log(`Query cost: ${cost}`);
},
formatErrorMessage: (cost) =>
`Query complexity ${cost} exceeds maximum allowed complexity of 1000`
})
]
});
# Nginx tarafında da rate limiting ekle
# /etc/nginx/conf.d/graphql-ratelimit.conf
limit_req_zone $binary_remote_addr zone=graphql:10m rate=30r/m;
server {
location /graphql {
limit_req zone=graphql burst=10 nodelay;
limit_req_status 429;
proxy_pass http://localhost:4000;
}
}
Tehdit 3: Batching ile Brute Force ve Rate Limit Atlatma
GraphQL’in toplu sorgu özelliği, rate limiting’i atlatmak için kullanılabilir. Normalde dakikada 100 istek sınırın varsa saldırgan tek bir HTTP isteğine 1000 sorgu sıkıştırabilir:
# Batching ile brute force örneği
curl -X POST https://api.example.com/graphql
-H "Content-Type: application/json"
-d '[
{"query": "mutation { login(email: "[email protected]", password: "pass1") { token } }"},
{"query": "mutation { login(email: "[email protected]", password: "pass2") { token } }"},
{"query": "mutation { login(email: "[email protected]", password: "pass3") { token } }"}
]'
Bu teknikle saldırgan tek HTTP isteğiyle binlerce şifre denemesi yapabilir.
Çözüm: Batching Limitlemesi ve Query Whitelisting
# Express middleware ile batching koruması
# middleware/graphqlSecurity.js
const MAX_BATCH_SIZE = 5;
const batchLimitMiddleware = (req, res, next) => {
if (Array.isArray(req.body)) {
if (req.body.length > MAX_BATCH_SIZE) {
return res.status(400).json({
error: `Batch size cannot exceed ${MAX_BATCH_SIZE} operations`
});
}
}
next();
};
# Query whitelisting için persisted queries kullan
# Apollo Client tarafında:
const client = new ApolloClient({
link: createPersistedQueryLink({
sha256,
useGETForHashedQueries: true
}).concat(httpLink),
});
# Server tarafında yalnızca hash'lenmiş sorguları kabul et
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
{
requestDidStart() {
return {
didResolveOperation({ request }) {
if (process.env.NODE_ENV === 'production') {
if (!request.extensions?.persistedQuery) {
throw new Error('Only persisted queries are allowed');
}
}
}
};
}
}
]
});
Tehdit 4: Authorization Açıkları ve IDOR
GraphQL’in en sık karşılaşılan güvenlik açıklarından biri yanlış authorization implementasyonu. Özellikle object level authorization eksikliği ciddi veri sızıntılarına yol açıyor. Şu senaryoya bak:
# Güvensiz resolver örneği - BUNU YAPMA
const resolvers = {
Query: {
# Kullanıcı kimliği doğrulanmış ama authorization yok
order: async (_, { id }, context) => {
if (!context.user) {
throw new Error('Not authenticated');
}
# Hangi kullanıcının siparişi olduğuna bakmıyor!
return await Order.findById(id);
}
}
};
# Güvenli resolver - her zaman böyle yaz
const resolvers = {
Query: {
order: async (_, { id }, context) => {
if (!context.user) {
throw new AuthenticationError('Authentication required');
}
const order = await Order.findById(id);
if (!order) {
throw new UserInputError('Order not found');
}
# Object level authorization kontrolü
if (order.userId.toString() !== context.user.id.toString()) {
if (!context.user.roles.includes('ADMIN')) {
throw new ForbiddenError('You can only access your own orders');
}
}
return order;
}
}
};
Direktif Tabanlı Authorization
Daha temiz bir yaklaşım için schema direktifleri kullanabilirsin:
# GraphQL şemasında direktif tanımla
# schema.graphql
directive @auth(requires: Role = USER) on FIELD_DEFINITION
enum Role {
ADMIN
MODERATOR
USER
}
type Query {
users: [User] @auth(requires: ADMIN)
myProfile: User @auth(requires: USER)
publicContent: [Content]
}
# Direktif implementasyonu
# directives/authDirective.js
const { mapSchema, getDirective, MapperKind } = require('@graphql-tools/utils');
const { defaultFieldResolver } = require('graphql');
function authDirectiveTransformer(schema) {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const authDirective = getDirective(schema, fieldConfig, 'auth')?.[0];
if (authDirective) {
const { requires } = authDirective;
const { resolve = defaultFieldResolver } = fieldConfig;
fieldConfig.resolve = async function(source, args, context, info) {
const user = context.user;
if (!user) {
throw new AuthenticationError('You must be logged in');
}
const roleHierarchy = ['USER', 'MODERATOR', 'ADMIN'];
const userRoleLevel = roleHierarchy.indexOf(user.role);
const requiredRoleLevel = roleHierarchy.indexOf(requires);
if (userRoleLevel < requiredRoleLevel) {
throw new ForbiddenError(
`This action requires ${requires} role`
);
}
return resolve(source, args, context, info);
};
}
return fieldConfig;
}
});
}
Tehdit 5: SQL ve NoSQL Injection
GraphQL resolver’ları içinde kullanıcı girdisini doğrudan sorguya eklemek felaket reçetesi. Bu hata düşündüğünden çok daha yaygın:
# Tehlikeli pattern - injection açığı
const resolvers = {
Query: {
searchUsers: async (_, { name }) => {
# Doğrudan string interpolation - KESİNLİKLE YAPMA
const query = `SELECT * FROM users WHERE name LIKE '%${name}%'`;
return await db.raw(query);
}
}
};
# Güvenli versiyon - parametrize sorgular kullan
const resolvers = {
Query: {
searchUsers: async (_, { name }, context) => {
# Input validasyonu
if (typeof name !== 'string' || name.length > 100) {
throw new UserInputError('Invalid search term');
}
# Özel karakterleri temizle
const sanitizedName = name.replace(/[%_\]/g, '\$&');
# Parametrize sorgu kullan
const users = await db('users')
.where('name', 'like', `%${sanitizedName}%`)
.limit(50);
return users;
}
}
};
# Input validation için Zod veya Joi kullan
# validators/userValidator.js
const { z } = require('zod');
const searchSchema = z.object({
name: z.string().min(1).max(100).regex(/^[a-zA-Z0-9s-_]+$/),
age: z.number().int().min(0).max(150).optional(),
email: z.string().email().optional()
});
const validateSearchInput = (input) => {
const result = searchSchema.safeParse(input);
if (!result.success) {
throw new UserInputError('Invalid input', {
validationErrors: result.error.flatten()
});
}
return result.data;
};
Tehdit 6: Bilgi Sızıntısı ve Hata Yönetimi
GraphQL’in varsayılan hata mesajları production’da çok fazla bilgi sızdırabilir. Stack trace’ler, veritabanı hataları, internal path bilgileri saldırganlara altın değerinde ipuçları verir.
# Güvenli hata yönetimi - Apollo Server
# utils/errorHandler.js
const { ApolloServerErrorCode } = require('@apollo/server/errors');
const formatError = (formattedError, error) => {
# Development'da tam hata göster
if (process.env.NODE_ENV === 'development') {
return formattedError;
}
# Production'da hataları sınıflandır
const originalError = error.originalError;
# Bilinen uygulama hataları - güvenli göster
if (
formattedError.extensions?.code === 'USER_INPUT_ERROR' ||
formattedError.extensions?.code === 'UNAUTHENTICATED' ||
formattedError.extensions?.code === 'FORBIDDEN'
) {
return {
message: formattedError.message,
extensions: {
code: formattedError.extensions.code
}
};
}
# Beklenmedik hatalar - detayları gizle, logla
console.error('Unexpected GraphQL error:', {
message: error.message,
stack: error.stack,
path: formattedError.path,
timestamp: new Date().toISOString()
});
return {
message: 'An unexpected error occurred',
extensions: {
code: 'INTERNAL_SERVER_ERROR'
}
};
};
const server = new ApolloServer({
typeDefs,
resolvers,
formatError
});
Tehdit 7: JWT Token Güvenliği ve Context Manipülasyonu
GraphQL context’inde token doğrulaması yaparken yapılan hatalar tüm güvenlik katmanını işe yaramaz hale getirebilir:
# Güvenli JWT context implementasyonu
# context/authContext.js
const jwt = require('jsonwebtoken');
const { AuthenticationError } = require('@apollo/server/errors');
const createContext = async ({ req }) => {
# Her istekte token'ı doğrula
const authHeader = req.headers.authorization || '';
const token = authHeader.startsWith('Bearer ')
? authHeader.slice(7)
: null;
if (!token) {
return { user: null };
}
try {
# Algorithm'ı açıkça belirt - 'none' algoritmasını engelle
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'],
issuer: 'your-app-name',
audience: 'your-app-users'
});
# Token'ın revoke edilip edilmediğini kontrol et
const isRevoked = await TokenBlacklist.exists({ token });
if (isRevoked) {
throw new Error('Token has been revoked');
}
# Kullanıcıyı veritabanından taze çek
const user = await User.findById(decoded.userId).select('-password');
if (!user || !user.isActive) {
throw new Error('User not found or inactive');
}
return { user, token };
} catch (error) {
# Token hatalıysa null user döndür, exception fırlatma
# Resolver'lar gerektiğinde authentication kontrolü yapacak
console.warn('JWT verification failed:', error.message);
return { user: null };
}
};
module.exports = { createContext };
Production Güvenlik Checklist
Tüm bu önlemleri uyguladıktan sonra deployment öncesi kontrol etmen gereken kritik noktalar:
- Introspection: Production ortamında tamamen kapalı olduğunu doğrula
- Query depth limiti: Maksimum 7-10 seviye olarak ayarlanmış olmalı
- Complexity limiti: Uygulamanın ihtiyacına göre kalibre edilmeli, genellikle 1000-5000 arası
- Rate limiting: Hem Nginx/HAProxy seviyesinde hem uygulama seviyesinde
- Batching: Maksimum batch size belirlenmeli, sensitive endpoint’lerde tamamen kapatılabilir
- HTTPS zorunluluğu: HTTP üzerinden GraphQL endpoint’i asla açık olmamalı
- CORS: Sadece bilinen origin’lere izin ver
- Logging: Her sorguyu operation name, kullanıcı ID ve maliyet ile logla
- Dependency taraması:
npm auditveyasnykile düzenli tarama yap - Schema review: Her deployment’ta şema değişikliklerini güvenlik perspektifinden incele
Monitoring ve Anomali Tespiti
Güvenlik sadece önlem almaktan ibaret değil, sürekli izleme de şart:
# GraphQL request logging middleware
# middleware/graphqlLogger.js
const graphqlLogger = {
requestDidStart(requestContext) {
const startTime = Date.now();
return {
didEncounterErrors({ errors }) {
errors.forEach(error => {
if (error.extensions?.code === 'UNAUTHENTICATED' ||
error.extensions?.code === 'FORBIDDEN') {
# Güvenlik olaylarını ayrı logla
securityLogger.warn({
type: 'AUTH_FAILURE',
ip: requestContext.request.http?.headers?.get('x-forwarded-for'),
userAgent: requestContext.request.http?.headers?.get('user-agent'),
operation: requestContext.request.operationName,
timestamp: new Date().toISOString()
});
}
});
},
willSendResponse() {
const duration = Date.now() - startTime;
# Çok uzun süren sorguları işaretle
if (duration > 5000) {
performanceLogger.warn({
type: 'SLOW_QUERY',
operation: requestContext.request.operationName,
duration,
query: requestContext.request.query?.substring(0, 500)
});
}
}
};
}
};
Sonuç
GraphQL güçlü bir araç ama bu güç sorumluluk gerektiriyor. REST API güvenliğini bilen birinin GraphQL’e geçişte en sık yaptığı hata, aynı güvenlik modelini uygulamaya çalışmak. GraphQL’in kendine özgü tehdit vektörleri var: introspection, depth bombing, batching abuse ve granüler authorization ihtiyacı bunların başında geliyor.
Bu yazıda anlattığım önlemlerin hepsini tek seferde implement etmek bunaltıcı gelebilir. Önce kritik olanlara odaklan: introspection’ı kapat, depth limit ekle, her resolver’da object level authorization uygula ve hata mesajlarını temizle. Bu dört adım bile büyük fark yaratır.
Güvenlik bir hedef değil, sürekli bir süreç. Şemanı değiştirdikçe, yeni resolver’lar ekledikçe bu kontrolleri tekrar gözden geçir. Benim önerim her sprint’in sonunda GraphQL şemasını güvenlik gözlüğüyle bir kez daha okumak. Çoğu açık karmaşık exploit tekniklerinden değil, gözden kaçan basit bir authorization kontrolünden kaynaklanıyor.
