CORS Nedir: API’de Cross-Origin Sorunlarını Çözme
Bir API geliştiriyorsunuz, backend’iniz mükemmel çalışıyor, Postman’dan testleriniz yeşil, ama tarayıcıdan istek attığınızda konsol kırmızıya dönüyor: “Access to fetch at ‘https://api.example.com’ from origin ‘https://app.example.com’ has been blocked by CORS policy”. Bu mesajı gören her geliştirici ve sysadmin’in içi bir an sıkışıyor. CORS, yanlış anlaşıldığında saatler kaybettiren, doğru anlaşıldığında ise beş dakikada çözülen bir mekanizma.
CORS Nedir ve Neden Var
CORS, yani Cross-Origin Resource Sharing, tarayıcıların farklı kaynaklardan (origin) gelen HTTP isteklerini nasıl yönettiğini belirleyen bir güvenlik mekanizması. Buradaki “origin” kavramı üç şeyin kombinasyonu: protokol, domain ve port. Yani https://app.example.com:443 ile https://api.example.com:443 farklı origin’ler. http://localhost:3000 ile http://localhost:8080 de farklı origin’ler, sadece port değişse bile.
Bu mekanizmanın temelinde Same-Origin Policy (SOP) yatıyor. Tarayıcılar, güvenlik nedeniyle bir web sayfasının sadece kendi origin’inden kaynak yükleyebileceğini varsayıyor. 1990’larda web daha basitti, ama bugün frontend ve backend ayrı domainlerde çalışıyor, CDN’ler var, microservice mimarileri var. SOP bu gerçeklikle çelişiyor. CORS, SOP’u tamamen kaldırmak yerine kontrollü bir şekilde gevşetmenin standardize edilmiş yolu.
Önemli bir nokta: CORS bir güvenlik açığı değil, güvenlik mekanizması. Yanlış yapılandırmak ise güvenlik açığı yaratır.
Same-Origin Policy Olmasa Ne Olur
Bunu anlamak için şu senaryoyu düşünün: Kullanıcı bankanızın sitesine giriş yaptı, oturum çerezi tarayıcıda duruyor. Sonra kötü niyetli bir siteye girdi. SOP olmadan, o kötü niyetli site arka planda bankanızın API’sine istek atabilir, çerezi otomatik gönderir ve işlem yapabilir. Buna CSRF saldırısı deniyor. SOP bu saldırıyı önlüyor.
CORS ise “tamam, bu spesifik güvenilir origin’e izin ver” diyebilmenizi sağlıyor.
Preflight İsteği: Asıl Karmaşıklık Buradan Geliyor
Tarayıcı her cross-origin isteği aynı şekilde yönetmiyor. Simple requests ve preflighted requests diye iki kategori var.
Simple request olma koşulları:
- Method: GET, HEAD veya POST
- Content-Type:
application/x-www-form-urlencoded,multipart/form-dataveyatext/plain - Özel header yok
Bu koşulları sağlamayan her şey için tarayıcı önce bir OPTIONS isteği gönderiyor. Bu preflight isteği sunucuya şunu soruyor: “Bu origin’den, bu method ve headerlarla istek atabilir miyim?” Sunucu olumlu yanıt verirse asıl istek gidiyor, vermezse bloklanıyor.
JSON gönderdiğiniz anda (Content-Type: application/json) zaten preflight tetikleniyor. Yani neredeyse tüm REST API iletişimi preflight gerektirir.
Nginx’te CORS Yapılandırması
Pek çok senaryoda backend uygulama sunucusunu değiştirmek yerine Nginx’te CORS yönetmek daha temiz ve merkezi bir çözüm.
server {
listen 443 ssl;
server_name api.example.com;
# Izin verilen origin'leri degiskende tut
set $cors_origin "";
if ($http_origin ~* "^https://(app.example.com|admin.example.com)$") {
set $cors_origin $http_origin;
}
location /api/ {
# Preflight OPTIONS istegini yakala
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With, X-API-Key' always;
add_header 'Access-Control-Max-Age' 86400 always;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
# Normal istekler icin CORS headerlari
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, X-Request-Id' always;
proxy_pass http://backend_upstream;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Burada always parametresine dikkat edin. Nginx, hata yanıtlarında (4xx, 5xx) add_header direktiflerini atlar. always eklemezseniz backend 500 döndürdüğünde CORS headerları gitmez, tarayıcı da CORS hatası görür, asıl hata gizlenir. Debugging’i mahveder.
Apache’de CORS Yapılandırması
Apache kullananlar için .htaccess veya virtual host konfigürasyonu:
<VirtualHost *:443>
ServerName api.example.com
# mod_headers ve mod_rewrite aktif olmali
# a2enmod headers rewrite
Header always set Access-Control-Allow-Origin "https://app.example.com"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Authorization, Content-Type, X-API-Key"
Header always set Access-Control-Max-Age "3600"
# Preflight isteklerini yakala
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]
ProxyPass /api/ http://localhost:8080/api/
ProxyPassReverse /api/ http://localhost:8080/api/
</VirtualHost>
Birden fazla origin’e izin vermek istiyorsanız Apache’de dinamik yöntem gerekiyor:
SetEnvIfNoCase Origin "^https://(app|admin|dashboard).example.com$" CORS_ORIGIN=$0
Header always set Access-Control-Allow-Origin "%{CORS_ORIGIN}e" env=CORS_ORIGIN
Header always set Vary "Origin"
Vary: Origin header’ını eklemek kritik. CDN ve proxy’ler, farklı origin’lerden gelen isteklerin farklı yanıt alabileceğini bu header sayesinde anlıyor. Bunu eklemezseniz CDN yanlış yanıtı cache’leyebilir.
Node.js / Express’te CORS
Backend’de yapılandırma yapmak zorundaysanız Express için temiz bir örnek:
# Paket kurulumu
npm install cors
# Veya elle yapılandirma
const express = require('express');
const app = express();
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
'http://localhost:3000' // sadece development'ta
];
const corsOptions = {
origin: function (origin, callback) {
// origin undefined ise server-to-server veya Postman istegi
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`CORS policy: ${origin} izin verilmedi`));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Authorization', 'Content-Type', 'X-API-Key'],
exposedHeaders: ['X-Total-Count', 'X-Request-Id'],
credentials: true,
maxAge: 86400 // preflight cache suresi (saniye)
};
app.use(cors(corsOptions));
// OPTIONS isteklerini hizli yanıtla
app.options('*', cors(corsOptions));
app.listen(8080);
Credentials ve Wildcard Tuzağı
CORS’un en sık düşülen tuzağı şu kombinasyon:
# BU CALISMIYOR - tarayici hata verir
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
credentials: true gönderildiğinde (cookie, Authorization header varsa) Access-Control-Allow-Origin kesinlikle wildcard * olamaz. Tam origin değeri olmalı. Tarayıcı bu kombinasyonu kasıtlı olarak reddediyor, çünkü “herkese izin ver + kimlik bilgilerini gönder” kombinasyonu ciddi güvenlik riski.
Doğru yapılandırma:
# Nginx ornegi - dinamik origin ile credentials
map $http_origin $cors_header {
default "";
"~^https://(app|admin).example.com$" $http_origin;
}
server {
location /api/ {
add_header 'Access-Control-Allow-Origin' $cors_header always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Vary' 'Origin' always;
}
}
Gerçek Dünya Senaryosu: Microservice Mimarisinde CORS
Bir e-ticaret platformu düşünelim: frontend https://shop.example.com‘da, ürün API’si https://products-api.example.com‘da, ödeme API’si https://payment-api.example.com‘da. Her servis ayrı yönetiliyor.
Bu durumda her serviste ayrı CORS yapılandırması yapmak yerine API Gateway kullanmak çok daha mantıklı. Kong, AWS API Gateway veya basit bir Nginx reverse proxy ile merkezi CORS yönetimi:
# Kong icin CORS plugin yapilandirmasi (declarative config)
plugins:
- name: cors
config:
origins:
- https://shop.example.com
- https://app.example.com
methods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
headers:
- Authorization
- Content-Type
- X-API-Key
exposed_headers:
- X-Total-Count
credentials: true
max_age: 3600
preflight_continue: false
Merkezi gateway yaklaşımı iki büyük avantaj sağlıyor: backend servisler CORS ile uğraşmıyor, sadece gateway güveniliyor. Ve tüm CORS politikası tek yerden yönetiliyor, tutarsızlık riski yok.
Development Ortamında CORS Sorunları
Yerel geliştirmede http://localhost:3000 frontend’i, http://localhost:8080 backend’i ile konuşmak istiyor. Production’da CORS açmak istemiyorsunuz ama development’ta da her seferinde uğraşmak istemiyorsunuz.
Birkaç yaklaşım var:
Webpack Dev Server veya Vite Proxy:
# vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^/api/, '')
}
}
}
}
Bu yaklaşımda tarayıcı aslında cross-origin istek yapmıyor. Frontend dev server proxy görevi görüyor, CORS devreye girmiyor. Production’da nginx upstream veya gerçek backend’i görüyor. Temiz çözüm.
Environment bazlı CORS:
# .env.development
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
# .env.production
CORS_ORIGINS=https://app.example.com,https://admin.example.com
# Node.js uygulamasinda
const allowedOrigins = process.env.CORS_ORIGINS.split(',');
CORS Hatalarını Debug Etme
Tarayıcı konsolundaki hata mesajları bazen yanıltıcı olabiliyor. Sistematik debug adımları:
1. Önce network sekmesini açın: Preflight OPTIONS isteğini bulun. Status kodu ne? 200 veya 204 bekliyorsunuz. 404 geliyorsa route yok, 405 geliyorsa OPTIONS method’u handle edilmiyor.
2. Response headerlarını kontrol edin:
# curl ile preflight simule edin
curl -v -X OPTIONS
-H "Origin: https://app.example.com"
-H "Access-Control-Request-Method: POST"
-H "Access-Control-Request-Headers: Authorization, Content-Type"
https://api.example.com/api/users
# Beklenen response headerlari:
# Access-Control-Allow-Origin: https://app.example.com
# Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
# Access-Control-Allow-Headers: Authorization, Content-Type
# Access-Control-Max-Age: 86400
3. Yaygın hata mesajlarını tanıyın:
- “No ‘Access-Control-Allow-Origin’ header is present”: Sunucu CORS headerı göndermemiş. Nginx/Apache konfigürasyonunu kontrol edin, OPTIONS isteği doğru handle ediliyor mu?
- “The value of the ‘Access-Control-Allow-Origin’ header must not be the wildcard ‘*'”: Credentials kullanıyorsunuz ama wildcard var.
- “Request header field X-Custom-Header is not allowed”: Preflight’ta
Access-Control-Allow-Headerslistesinde eksik header var. - “Method DELETE is not allowed”:
Access-Control-Allow-Methodslistesinde eksik method.
# Tum CORS headerlarini kontrol eden basit bash scripti
check_cors() {
local url=$1
local origin=${2:-"https://test.example.com"}
echo "=== Preflight Kontrolu ==="
curl -sI -X OPTIONS
-H "Origin: $origin"
-H "Access-Control-Request-Method: POST"
-H "Access-Control-Request-Headers: Authorization,Content-Type"
"$url" | grep -i "access-control|vary|content-type"
echo ""
echo "=== Basit GET Istegi ==="
curl -sI -X GET
-H "Origin: $origin"
"$url" | grep -i "access-control|vary"
}
check_cors "https://api.example.com/health"
Güvenlik Açısından CORS Yapılandırması
CORS güvenlik mekanizması olduğundan yanlış yapılandırmak ciddi riskler yaratıyor.
Tehlikeli yapılandırmalar:
*wildcard credentials ile beraber: Yukarıda anlattık, zaten çalışmıyor ama bazıları bypass yolu arıyor.- Origin’i body’den veya header’dan okuyup doğrulamadan echo etmek:
Access-Control-Allow-Origin: [saldirgan-site.com]yansıtmak, saldırganın istediği origin’e izin vermek demek. nullorigin’e izin vermek:file://protokolü ve bazı redirect senaryolarında originnullgeliyor.Access-Control-Allow-Origin: nulleklemek sandbox’lı iframe’lerden saldırıya kapı açıyor.- Subdomain wildcard’ı yanlış regex ile yazmak:
.example.comyerine.example.comyazmak,evilexample.comvenotexample.com‘u da kapsıyor.
Güvenli regex örneği:
# Nginx - subdomain'lere izin verirken dikkatli olun
map $http_origin $cors_origin {
default "";
# YANLIS: notexample.com'u da yakalar
# "~example.com$" $http_origin;
# DOGRU: sadece example.com ve subdomainleri
"~^https://([a-z0-9-]+.)?example.com$" $http_origin;
}
Minimum yetki prensibi: Sadece gerçekten ihtiyaç duyulan origin’lere, method’lara ve header’lara izin verin. “Çalışsın da nasıl çalışırsa” diyerek her şeye izin vermek, bir gün başınıza dert açar.
Max-Age ile Preflight Optimizasyonu
Her istek öncesi preflight gönderilmesi ciddi bir performans sorunu. 100 API isteği için 100 preflight demek, ağ trafiğini ikiye katlıyor.
Access-Control-Max-Age header’ı tarayıcıya “bu preflight sonucunu bu kadar saniye cache’le” diyor.
# Nginx ornegi
add_header 'Access-Control-Max-Age' 86400 always; # 24 saat
# Chrome maksimum 7200 saniye (2 saat) cache'liyor
# Firefox maksimum 86400 saniye (24 saat) cache'liyor
# Safari 600 saniye ile sinirli
Development ortamında bu değeri düşük tutun, yoksa yapılandırma değişiklikleriniz hemen yansımaz.
Sonuç
CORS, anlaşıldığında korkutucu değil, mantıklı bir güvenlik mekanizması. Temel prensipleri tekrar özetleyelim:
- Same-Origin Policy tarayıcının temel güvenlik katmanı, CORS onu kontrollü biçimde gevşetiyor.
- Preflight isteği, JSON veya özel header kullanan neredeyse tüm API isteklerini etkiliyor. OPTIONS handler’ı yazmayı unutmayın.
credentials: trueile wildcard*origin birlikte kullanılamaz, kesin origin değeri gerekli.Vary: Originheader’ını CDN veya proxy varsa mutlaka ekleyin.- Nginx veya Apache’de
alwaysparametresi olmadan hata yanıtlarında CORS headerları gitmez. - Üretim ortamında minimum yetki prensibine uyun, gereksiz origin, method ve header’lara izin vermeyin.
- Debug için önce
curlile preflight simüle edin, tarayıcı mesajlarına körce güvenmeyin.
Microservice mimarisinde büyüdükçe CORS yönetimini API Gateway katmanına taşımak, onlarca serviste ayrı ayrı konfigürasyon tutmaktan çok daha sürdürülebilir. Her şeyi merkezi bir noktadan yönetmek hem tutarlılığı artırıyor hem de güvenlik açığı riskini azaltıyor.
CORS hataları can sıkıcı görünüyor ama altında yatan “hangi kaynak bu kaynağa erişebilir” sorusu son derece meşru. Bu soruyu doğru cevaplamak, hem güvenli hem de işlevsel bir API ortamının temel taşlarından biri.
