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-data veya text/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-Headers listesinde eksik header var.
  • “Method DELETE is not allowed”: Access-Control-Allow-Methods listesinde 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.
  • null origin’e izin vermek: file:// protokolü ve bazı redirect senaryolarında origin null geliyor. Access-Control-Allow-Origin: null eklemek sandbox’lı iframe’lerden saldırıya kapı açıyor.
  • Subdomain wildcard’ı yanlış regex ile yazmak: .example.com yerine .example.com yazmak, evilexample.com ve notexample.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: true ile wildcard * origin birlikte kullanılamaz, kesin origin değeri gerekli.
  • Vary: Origin header’ını CDN veya proxy varsa mutlaka ekleyin.
  • Nginx veya Apache’de always parametresi 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 curl ile 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.

Bir yanıt yazın

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