GraphQL API’da CORS Yapılandırması
Modern web uygulamalarında GraphQL API’ları giderek daha yaygın hale geliyor. Ama ne kadar güçlü bir API tasarlarsanız tasarlayın, CORS yapılandırmasını doğru yapmazsanız frontend uygulamalarınız o API’ye erişemez ya da daha kötüsü, güvenlik açıkları doğurursunuz. Bu yazıda GraphQL API’larınızda CORS’u nasıl doğru yapılandıracağınızı, yaygın hataları nasıl önleyeceğinizi ve production ortamı için en iyi pratikleri ele alacağız.
CORS Nedir ve GraphQL’de Neden Önemlidir
CORS (Cross-Origin Resource Sharing), tarayıcıların farklı origin’lerden gelen istekleri nasıl ele alacağını belirleyen bir güvenlik mekanizmasıdır. Örneğin https://app.sirketim.com adresindeki frontend uygulamanız https://api.sirketim.com adresindeki GraphQL API’nıza istek atmaya çalıştığında, tarayıcı önce bir “preflight” isteği gönderir. Sunucu bu isteğe doğru CORS başlıklarıyla yanıt vermezse tarayıcı asıl isteği engelleyerek bir hata fırlatır.
GraphQL’de CORS meselesi biraz daha karmaşık bir boyut kazanıyor. REST API’lardan farklı olarak GraphQL genellikle tek bir endpoint üzerinden çalışır (/graphql). Bu endpoint hem query hem mutation hem de subscription isteklerini karşılar. Üstelik GraphQL Playground veya Apollo Studio gibi araçlarla geliştirme yaparken farklı origin’lerden istek atmanız çok sık karşılaşılan bir durumdur.
Bir diğer kritik nokta şu: GraphQL mutations, veritabanında veri değiştiren işlemler yapabilir. Eğer CORS’u çok geniş tutarsanız kötü niyetli siteler kullanıcılarınız adına mutation istekleri gönderebilir. Bu da CSRF (Cross-Site Request Forgery) saldırılarına kapı aralar.
Temel CORS Başlıkları
Doğru yapılandırma yapabilmek için önce hangi HTTP başlıklarıyla iş yaptığımızı anlamamız gerekiyor.
- Access-Control-Allow-Origin: Hangi origin’lerin bu kaynağa erişebileceğini belirtir.
*yazmak tüm origin’lere kapıyı açar; bu production’da genellikle kabul edilemez. - Access-Control-Allow-Methods: İzin verilen HTTP metodlarını listeler. GraphQL için en azından
POSTveOPTIONSgereklidir. - Access-Control-Allow-Headers: İstemcinin gönderebileceği başlıkları tanımlar.
Content-TypeveAuthorizationen yaygın ihtiyaç duyulanlardır. - Access-Control-Allow-Credentials: Cookie veya Authorization başlığının cross-origin isteklerde iletilip iletilmeyeceğini kontrol eder.
- Access-Control-Max-Age: Preflight yanıtının tarayıcı tarafından kaç saniye önbelleğe alınacağını belirtir.
- Access-Control-Expose-Headers: Tarayıcının JavaScript koduna açabileceği yanıt başlıklarını tanımlar.
Node.js ve Express ile GraphQL CORS Yapılandırması
En yaygın kullanılan setup’lardan biri Express üzerinde Apollo Server çalıştırmaktır. Basit bir yapılandırmadan başlayalım:
npm install apollo-server-express express cors
# server.js - Temel CORS yapılandırması
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const cors = require('cors');
const app = express();
// İzin verilen origin listesi
const allowedOrigins = [
'https://app.sirketim.com',
'https://admin.sirketim.com',
'http://localhost:3000',
'http://localhost:4000'
];
const corsOptions = {
origin: function (origin, callback) {
// Origin yoksa (curl gibi araçlar veya same-origin istekler)
if (!origin) return callback(null, true);
if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error(`CORS policy: ${origin} bu API'ye erişemez`));
}
},
credentials: true,
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
exposedHeaders: ['X-Total-Count', 'X-Page-Info'],
maxAge: 86400 // 24 saat
};
// CORS middleware'ini Apollo Server'dan önce uygula
app.use(cors(corsOptions));
app.options('*', cors(corsOptions)); // Preflight istekleri için
async function startServer() {
const server = new ApolloServer({
typeDefs,
resolvers,
// Apollo Server'ın kendi CORS'unu devre dışı bırak
// çünkü biz Express seviyesinde yönetiyoruz
});
await server.start();
server.applyMiddleware({
app,
path: '/graphql',
cors: false // Express'teki cors middleware'ini kullan
});
app.listen(4000, () => {
console.log('GraphQL API 4000 portunda çalışıyor');
});
}
startServer();
Burada dikkat etmeniz gereken kritik nokta cors: false ayarıdır. Apollo Server’ın kendi CORS yönetimini devre dışı bırakıp işi Express middleware’ine bırakıyoruz. İkisini aynı anda aktif tutarsanız çakışmalar yaşabilirsiniz.
Apollo Server v4 ile Standalone Yapılandırma
Apollo Server’ın dördüncü sürümüyle birlikte standalone server yapısı geldi. Bu yapıda CORS yapılandırması biraz farklı:
# Apollo Server v4 standalone CORS yapılandırması
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
const server = new ApolloServer({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
context: async ({ req }) => {
// Context oluşturma
const token = req.headers.authorization || '';
return { token };
},
});
// Standalone server ile CORS için node-http-server kullanımı
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const { ApolloServerPluginDrainHttpServer } = require('@apollo/server/plugin/drainHttpServer');
const express = require('express');
const http = require('http');
const cors = require('cors');
const app = express();
const httpServer = http.createServer(app);
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
});
await server.start();
app.use(
'/graphql',
cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true,
}),
express.json(),
expressMiddleware(server, {
context: async ({ req }) => ({ token: req.headers.authorization }),
}),
);
await new Promise((resolve) => httpServer.listen({ port: 4000 }, resolve));
console.log(`GraphQL API çalışıyor: http://localhost:4000/graphql`);
Ortam Bazlı Dinamik CORS Yapılandırması
Production, staging ve development ortamlarının farklı CORS politikalarına ihtiyacı var. Bu durumu environment variable’larla yönetmek en temiz çözüm:
# .env.development
NODE_ENV=development
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:4000,http://localhost:8080
CORS_MAX_AGE=600
# .env.staging
NODE_ENV=staging
ALLOWED_ORIGINS=https://staging-app.sirketim.com,https://staging-admin.sirketim.com
CORS_MAX_AGE=3600
# .env.production
NODE_ENV=production
ALLOWED_ORIGINS=https://app.sirketim.com,https://admin.sirketim.com
CORS_MAX_AGE=86400
# cors-config.js - Ortam bazlı CORS yapılandırması
const getCorsConfig = () => {
const isDevelopment = process.env.NODE_ENV === 'development';
const allowedOrigins = process.env.ALLOWED_ORIGINS
? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim())
: [];
return {
origin: (origin, callback) => {
// Development ortamında localhost'a her zaman izin ver
if (isDevelopment && (!origin || origin.startsWith('http://localhost'))) {
return callback(null, true);
}
if (!origin) {
// Server-to-server istekleri veya Postman gibi araçlar
return callback(null, true);
}
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
console.warn(`CORS ihlali: ${origin} adresinden istek reddedildi`);
callback(new Error('CORS politikası ihlal edildi'));
}
},
credentials: true,
methods: ['POST', 'OPTIONS'],
allowedHeaders: [
'Content-Type',
'Authorization',
'X-Requested-With',
'X-Apollo-Operation-Name',
'Apollo-Require-Preflight'
],
maxAge: parseInt(process.env.CORS_MAX_AGE || '3600'),
optionsSuccessStatus: 200
};
};
module.exports = { getCorsConfig };
GraphQL Subscription’larında CORS
WebSocket tabanlı subscription’lar için CORS biraz farklı çalışır. WebSocket protokolü, HTTP CORS kurallarına tabi değildir; bunun yerine Origin başlığını kontrol etmeniz gerekir:
# WebSocket subscription CORS yapılandırması
const { WebSocketServer } = require('ws');
const { useServer } = require('graphql-ws/lib/use/ws');
const { execute, subscribe } = require('graphql');
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql',
// WebSocket bağlantılarında origin kontrolü
verifyClient: (info, callback) => {
const origin = info.origin;
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
if (!origin || allowedOrigins.includes(origin)) {
callback(true);
} else {
console.warn(`WebSocket CORS ihlali: ${origin}`);
callback(false, 403, 'Forbidden: CORS politikası');
}
}
});
const serverCleanup = useServer(
{
schema,
context: async (ctx) => {
// WebSocket context'inde token doğrulama
const token = ctx.connectionParams?.authorization;
if (!token) {
throw new Error('Yetkilendirme gerekli');
}
return { token };
},
onConnect: async (ctx) => {
console.log(`WebSocket bağlantısı: ${ctx.extra.request.headers.origin}`);
},
},
wsServer
);
Nginx ile GraphQL CORS Yapılandırması
Pek çok production ortamında uygulama sunucusunun önünde Nginx çalışır. CORS’u Nginx seviyesinde yönetmek hem performanslı hem de merkezi bir çözüm sunar:
# /etc/nginx/sites-available/graphql-api.conf
server {
listen 443 ssl http2;
server_name api.sirketim.com;
ssl_certificate /etc/letsencrypt/live/api.sirketim.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.sirketim.com/privkey.pem;
# CORS değişkenlerini tanımla
set $cors_origin "";
set $cors_methods "POST, OPTIONS";
set $cors_headers "Content-Type, Authorization, X-Requested-With";
# İzin verilen origin kontrolü
if ($http_origin ~* "^https://(app|admin).sirketim.com$") {
set $cors_origin $http_origin;
}
location /graphql {
# Preflight OPTIONS isteği
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' $cors_methods always;
add_header 'Access-Control-Allow-Headers' $cors_headers always;
add_header 'Access-Control-Max-Age' 86400 always;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
# Asıl istek için CORS başlıkları
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Expose-Headers' 'X-Total-Count' always;
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_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache_bypass $http_upgrade;
}
}
CORS Hatalarını Test Etme ve Debug
CORS sorunlarını tespit etmek için birkaç pratik yöntem var:
# Preflight isteğini manuel test etme
curl -v -X OPTIONS
-H "Origin: https://app.sirketim.com"
-H "Access-Control-Request-Method: POST"
-H "Access-Control-Request-Headers: Content-Type, Authorization"
https://api.sirketim.com/graphql
# Gerçek bir GraphQL isteğini test etme
curl -v -X POST
-H "Origin: https://app.sirketim.com"
-H "Content-Type: application/json"
-H "Authorization: Bearer TOKEN_BURAYA"
-d '{"query": "{ __typename }"}'
https://api.sirketim.com/graphql
# CORS başlıklarını kontrol etme
curl -I -X POST
-H "Origin: https://izinsiz-site.com"
-H "Content-Type: application/json"
https://api.sirketim.com/graphql
CORS hatalarını loglara yazdırmak için özel bir middleware ekleyebilirsiniz:
# cors-logger.js - CORS hata loglama middleware'i
const corsLogger = (req, res, next) => {
const origin = req.headers.origin;
const method = req.method;
res.on('finish', () => {
const corsHeader = res.getHeader('Access-Control-Allow-Origin');
if (origin && !corsHeader) {
console.error({
timestamp: new Date().toISOString(),
type: 'CORS_VIOLATION',
origin: origin,
method: method,
path: req.path,
statusCode: res.statusCode
});
}
if (method === 'OPTIONS') {
console.log({
timestamp: new Date().toISOString(),
type: 'PREFLIGHT',
origin: origin,
allowedOrigin: corsHeader,
statusCode: res.statusCode
});
}
});
next();
};
module.exports = corsLogger;
Güvenlik En İyi Pratikleri
CORS yapılandırmasında güvenlik açısından dikkat etmeniz gereken bazı kritik noktalar var:
- Wildcard kullanmaktan kaçının: Production’da
Access-Control-Allow-Origin: *kullanmak çok tehlikelidir, özelliklecredentials: trueile birlikte kullanılamaz zaten ama kötü alışkanlık yaratır. - Origin listesini düzenli güncelleyin: Uygulamadan ayrılan veya değişen domain’leri hemen listeden çıkarın.
- Regex ile origin doğrulamada dikkatli olun:
sirketim.comregex’ikötüsirketim.comadresini de geçirebilir. Regex’lerinizi sıkı tutun. - Credentials ile birlikte wildcard kullanılamaz:
credentials: trueayarladığınızdaAccess-Control-Allow-Origin: *geçersiz olur, tarayıcı isteği reddeder. - Apollo’nun introspection’ını production’da kapatın: CORS’tan bağımsız olarak production ortamında schema bilgisinin açıkta olması güvenlik riski taşır.
- Rate limiting ekleyin: CORS politikanızı geçen kaynaklardan gelen istekler için de rate limiting uygulamak şarttır.
Gerçek Dünya Senaryosu: Microservice Mimarisinde CORS
Birden fazla frontend uygulamasının tek bir GraphQL gateway’e bağlandığı bir microservice mimarisini düşünün. Bu senaryoda merkezi bir CORS konfigürasyonu oluşturmak mantıklıdır:
# central-cors-config.js
// Redis veya config service'ten yüklenebilir
const loadAllowedOrigins = async () => {
// Gerçek senaryoda bu liste bir config servisinden veya
// Redis'ten dinamik olarak yüklenebilir
const staticOrigins = (process.env.ALLOWED_ORIGINS || '').split(',').map(o => o.trim());
// Wildcard subdomain desteği
const allowedPatterns = [
/^https://[w-]+.sirketim.com$/,
/^https://[w-]+.staging.sirketim.com$/,
];
return {
staticOrigins,
allowedPatterns
};
};
const createDynamicCorsMiddleware = async () => {
const { staticOrigins, allowedPatterns } = await loadAllowedOrigins();
return cors({
origin: (origin, callback) => {
if (!origin) return callback(null, true);
// Statik liste kontrolü
if (staticOrigins.includes(origin)) {
return callback(null, true);
}
// Pattern kontrolü
const isAllowedPattern = allowedPatterns.some(pattern => pattern.test(origin));
if (isAllowedPattern) {
return callback(null, true);
}
callback(new Error(`${origin} CORS politikasınca reddedildi`));
},
credentials: true,
methods: ['POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Client-Name', 'X-Client-Version'],
maxAge: 3600
});
};
module.exports = { createDynamicCorsMiddleware };
Sonuç
GraphQL API’larında CORS yapılandırması ilk bakışta basit gibi görünse de, yanlış yapıldığında ciddi güvenlik açıklarına veya çalışmayan uygulamalara yol açar. En temel kural şu: her zaman en kısıtlayıcı politikadan başlayın, ihtiyaç duydukça genişletin.
Production ortamı için özet kontrol listesi:
- Wildcard origin kullanmayın, her zaman belirli origin’leri listeleyin
- Apollo Server’ın CORS’unu kapatıp Express veya Nginx seviyesinde yönetin
- Environment variable’larla her ortam için farklı CORS konfigürasyonu oluşturun
- WebSocket subscription’larında
verifyClientile origin kontrolü yapın - CORS ihlallerini loglayın, böylece kimin nereye erişmeye çalıştığını görün
- Preflight isteklerini curl ile test edin her deploy sonrasında
- Nginx üzerinden CORS yönetimini değerlendirin, uygulama kodunu sadeleştirir
CORS’u doğru yapılandırmak bir kez yapılıp unutulacak bir iş değil. Yeni frontend uygulaması eklendiğinde, domain değiştiğinde veya yeni bir ortam kurulduğunda mutlaka CORS konfigürasyonunu güncellemeniz gerekiyor. Bunu otomatize etmek için config servislerine yatırım yapmak, özellikle büyük ekiplerde çok işe yarıyor.
