Ana Sayfa / Blog / CORS’u gerçekten anlamak: preflight, credentials, wildcard

CORS’u gerçekten anlamak: preflight, credentials, wildcard

CORS error'ları her frontend developer'ın karşılaştığı klasik. Gerçekten nasıl çalışıyor, hangi header'lar ne yapıyor?

CORS (Cross-Origin Resource Sharing) web development’in en çok yanlış anlaşılan kavramlarından biri. Frontend developer “bir CORS error var” diyor, backend developer random header’lar ekliyor, sorun bazen çözülüyor bazen değil. Bu pattern’i kırmak için CORS’un nasıl çalıştığını gerçekten anlamak lazım.

15+ yıldır full-stack development yapıyorum, onlarca CORS issue debug ettim. Bu yazıda CORS mental model’ini ve common pitfalls’ı anlatacağım.

CORS ne için var?

Browser Same-Origin Policy (SOP) web’in güvenlik temeli. Bir origin (protocol + domain + port) başka origin’in kaynaklarını okuyamıyor. Bu kural olmasa:

  • example.com’u ziyaret edince kötü niyetli example.com kodu bank.com’a senin cookie’lerinle request atardı
  • Password’ün çalınabilirdi, hesap bilgilerin çekilebilirdi

SOP bunu engelliyor. Ama legitimate cross-origin ihtiyaçları da var: your-frontend.com’un your-api.com’a request atması.

CORS bu legitimate senaryoları enable eden mekanizma. Browser ile server arasında negotiate ediyor: “bu origin’den gelen request’i kabul ediyor musun?”

Simple request vs preflight

CORS iki kategoriye ayrılıyor:

Simple requests: Şu koşulları sağlıyorsa browser direct gönderiyor (preflight yok):
– Method: GET, HEAD, or POST
– Content-Type: application/x-www-form-urlencoded, multipart/form-data, text/plain
– Sadece standard header’lar (Accept, Content-Type, etc.)

Preflight requests: Yukarıdaki kriterler dışındaki her şey. Browser önce OPTIONS request atıyor, server “evet, bu request’i kabul ediyorum” derse gerçek request gidiyor.

Modern API’larda genelde JSON body + PUT/DELETE kullanılıyor, preflight default.

Preflight nasıl çalışıyor?

Browser API’ya POST request atacak, Content-Type: application/json ile. Simple değil, preflight gerekli.

Step 1: Browser OPTIONS gönderiyor:

OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

Step 2: Server cevaplıyor:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, GET, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

Step 3: Browser gerçek POST gönderiyor:

POST /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer token...

{...}

Preflight’i unutma: her complex request’te bu cycle. Extra latency var ama 1 kez. Max-Age ile browser response’u cache ediyor (default 5 dakika, browser-specific).

Yaygın CORS hataları

Hata 1: “No ‘Access-Control-Allow-Origin’ header is present”

En sık hata. Backend CORS header göndermiyor veya browser’ın beklediği origin’i göndermiyor.

Fix:

Access-Control-Allow-Origin: https://app.example.com

veya wildcard (dikkat! aşağıya bakın):

Access-Control-Allow-Origin: *

Hata 2: Wildcard with credentials

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

Browser bunu reject ediyor. Wildcard ile credentials (cookie, Authorization header) kombine edilemiyor.

Fix: Specific origin kullanmalı:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

Multiple origin destekliyorsan, request’te gelen Origin’i dynamically echo et (validation ile).

Hata 3: OPTIONS request authentication’a takılıyor

Authentication middleware OPTIONS request’i de block ediyor. Preflight check yapılmadan 401 dönüyor.

Fix: OPTIONS request’i authentication middleware’den muaf tut.

# Example (Python/Flask-style)
@app.before_request
def authenticate():
    if request.method == 'OPTIONS':
        return  # Skip auth for preflight
    # ... normal auth ...

Hata 4: Header’ı allow etmiyorsun

Frontend custom header gönderiyor (örn. X-Requested-With). Backend Access-Control-Allow-Headers‘ta bu header’ı listede tutmuyor.

Fix:

Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With

Tüm custom header’ları explicitly listele.

Hata 5: Response header’ı expose etmemek

Default olarak browser sadece basic header’ları JavaScript’e expose ediyor. Custom header (örn. X-Total-Count) için:

Access-Control-Expose-Headers: X-Total-Count, X-Pagination-Info

Bu olmadan frontend custom header’ları okuyamıyor.

Credentials mode

Fetch API’da credentials: 'include' ile cookie/auth header gönderiliyor.

Frontend:

fetch('https://api.example.com/users', {
    credentials: 'include',
    headers: {'Content-Type': 'application/json'}
})

Backend:

Access-Control-Allow-Origin: https://app.example.com  // NOT *
Access-Control-Allow-Credentials: true

Credentials mode’da wildcard yok. Specific origin zorunlu.

Wildcard vs specific origin

Wildcard (*):
– Simple: tüm origin’lere izin
– Credentials kullanılamaz
– Public API’lar için OK (örn. content API, no auth)

Specific origin (domain):
– Sadece belirli domain’den izin
– Credentials kullanılabilir
– Secure, recommended for authenticated APIs

Multiple origin destekliyorsan whitelist kur:

allowed_origins = ['https://app.example.com', 'https://staging.example.com']

@app.after_request
def add_cors_headers(response):
    origin = request.headers.get('Origin')
    if origin in allowed_origins:
        response.headers['Access-Control-Allow-Origin'] = origin
        response.headers['Access-Control-Allow-Credentials'] = 'true'
    return response

Subdomain CORS

app.example.comapi.example.com request’i de cross-origin. Subdomain aynı bile olsa browser bunu cross olarak görüyor.

Fix aynı: specific origin whitelist.

Cookie tarafında: cookie’yi .example.com domain’ine yazıyorsan (leading dot), subdomain’ler erişiyor. example.com domain’ine yazarsan sadece aynı origin.

CORS vs CSRF

Sıkça karışıyor. Farkları:

CORS: Browser’ın cross-origin request’lere izin verip vermediğini kontrol ediyor. Server’ın JavaScript’e hangi response’u göstereceğini belirliyor.

CSRF: User’ın bilgisi olmadan cross-site request gönderilmesini önleyen pattern. CSRF token ile mitigate.

CORS güvenlik değil, access control. CSRF saldırısını CORS engelliyor ama CORS yeterli değil (legacy browser, specific scenarios). CSRF token hâlâ gerekli.

Development’ta CORS

Local development’ta API farklı port’ta:
– Frontend: localhost:3000
– Backend: localhost:8080

İkisi farklı origin. CORS hatası.

Fix options:

1. Backend’de development CORS:

if environment == 'development':
    allowed_origins.append('http://localhost:3000')

2. Proxy: Webpack dev server / Vite proxy ile frontend aynı origin’de gibi davranıyor.

// vite.config.js
export default {
    server: {
        proxy: {
            '/api': 'http://localhost:8080'
        }
    }
}

Frontend’den /api/users → Vite proxy → localhost:8080/api/users. Same-origin’den gidiyor gibi görünüyor.

Production’da proxy yok, CORS config production’da kurulacak.

Debugging CORS

Network tab’da preflight’ı görebilirsin:

  1. OPTIONS request var mı? Yoksa browser preflight sending etmiyor.
  2. OPTIONS response code ne? 200/204 olmalı. 401/403 ise auth issue.
  3. Access-Control-Allow-Origin header ne? Request Origin ile match mı?
  4. Access-Control-Allow-Methods method’u listeliyor mu?
  5. Access-Control-Allow-Headers custom header’ları listeliyor mu?

Bu 5 soruyu cevaplayınca CORS issue’larının %95’i çözülüyor.

Sonuç

CORS browser’ın güvenlik mekanizması, server restrictions değil. Backend doğru header’ları göndermezse browser request’i block ediyor.

Key concepts: simple vs preflight, credentials restriction, specific origin vs wildcard. Bunları anladığında debug 5 dakikada bitiyor.

Production’da CORS config dikkatli kur: specific origins, minimal allowed methods/headers, credentials needed only when required. Security ve UX dengesi.

Bu konuda bir projeniz mi var?

Kısa bir özet bırakın, 24 saat içinde size dönüş yapayım.

İletişime Geç