CORS (Cross-Origin Resource Sharing, kökenler arası kaynak paylaşımı), bir sunucunun kendi kökeni dışındaki hangi kökenlerin tarayıcıda kendi yanıtlarını okuyabileceğini HTTP başlıklarıyla bildirdiği mekanizmadır. Tarayıcılar güvenlik gereği, betiklerin başka bir kökenden okuduğu yanıtları varsayılan olarak engeller. Sunucu Access-Control-Allow-Origin başlığıyla sizin kökeninize izin verirse tarayıcı yanıtı kodunuza teslim eder; vermezse istek gitmiş olsa bile yanıt size gösterilmez.

Bu yazıda köken kavramını, aynı köken politikasını, basit istek ile ön uçuşlu (preflight) istek ayrımını, kimlik bilgili istekleri ve sunucunun göndermesi gereken Access-Control-* başlıklarını örneklerle anlatıyoruz. Tarayıcı konsolunda gördüğünüz belirli hata mesajlarının tek tek çözümü için CORS hataları ve çözümleri yazısına, istek tarafındaki fetch() kullanımı için Fetch API rehberine bakabilirsiniz.

Köken (origin) nedir?

Köken (origin) nedir?

İki URL'nin aynı kökene sahip olması için protokolün (şema), ana makine adının (host) ve bağlantı noktasının (port) aynı olması gerekir. Yol (path), sorgu dizesi ve parça kökeni etkilemez. MDN'nin örneğinde http://store.company.com/dir/page.html sayfası temel alınır. URL nesnesinin origin özelliğiyle bu karşılaştırmayı kendiniz yapabilirsiniz:

javascript
const sayfa = new URL("http://store.company.com/dir/page.html");
const adaylar = [
  "http://store.company.com/dir2/other.html",
  "http://store.company.com:80/dir/inner/another.html",
  "https://store.company.com/page.html",
  "http://store.company.com:81/dir/page.html",
  "http://news.company.com/dir/page.html",
];
for (const adres of adaylar) {
  const hedef = new URL(adres);
  console.log(hedef.origin === sayfa.origin, hedef.origin);
}
// true http://store.company.com
// true http://store.company.com
// false https://store.company.com
// false http://store.company.com:81
// false http://news.company.com

İkinci satırda açıkça :80 yazılmasına rağmen köken aynı çıkar, çünkü http için varsayılan port zaten 80'dir. https'e geçmek, portu değiştirmek veya alt alan adı kullanmak ise kökeni değiştirir. Yani api.siteniz.com ile www.siteniz.com farklı kökenlerdir ve aralarındaki fetch() istekleri CORS kurallarına tabidir.

Aynı köken politikası ve CORS'un rolü

Aynı köken politikası, bir kökenden yüklenen belge veya betiğin başka bir kökenden gelen kaynaklarla etkileşimini sınırlar. MDN amacını şöyle anlatır: kötü niyetli bir site, kullanıcının oturum açtığı bir e-posta servisinin veya dışarıdan erişilemeyen bir şirket ağının verisini tarayıcıda okuyup saldırgana aktaramamalıdır.

Kökenler arası etkileşimler kabaca üç gruba ayrılır. Yazma işlemleri (bağlantılar, yönlendirmeler, form gönderimleri) genellikle serbesttir. Gömme işlemleri (<script>, <img>, <video>, stil dosyaları, <iframe>) de genellikle serbesttir. Okuma işlemleri ise genellikle yasaktır. CORS tam olarak bu okuma yasağını, sunucunun izin verdiği durumlarda gevşeten mekanizmadır.

CORS kullanan istekler arasında fetch() ve XMLHttpRequest çağrıları, @font-face ile yüklenen web fontları, crossorigin özelliği taşıyan elemanların istekleri, modül betikleri, WebGL dokuları ve drawImage() ile canvas'a çizilip piksel verisi okunacak görseller bulunur. Canvas tarafındaki kirlenme (tainted canvas) konusu canvas görsel ve piksel işlemleri yazısında anlatılıyor.

Basit istekler

Basit istekler: Yöntem GET, HEAD veya POST olmalı., Tarayıcının kendi eklediği başlıklar dışında yalnızca CORS güvenli listesindeki..., Content-Type değeri yalnızca...

Bazı istekler ön uçuş isteği tetiklemez. Eski CORS belirtiminde bunlara basit istek (simple request) deniyordu; bugün CORS'u tanımlayan Fetch belirtimi bu terimi kullanmasa da kavram aynıdır. Mantığı şudur: HTML formları zaten her kökene bu tür istekler gönderebildiği için sunucuların CSRF'e karşı korunması gerekir; dolayısıyla bu isteklerin gönderilmesi için ayrıca izin istenmez. Ancak yanıtın betiğe gösterilmesi için sunucu yine Access-Control-Allow-Origin ile izin vermelidir. Bir isteğin basit sayılması için şu koşulların hepsi sağlanmalıdır:

  • Yöntem GET, HEAD veya POST olmalı.
  • Tarayıcının kendi eklediği başlıklar dışında yalnızca CORS güvenli listesindeki başlıklar elle ayarlanmalı: Accept, Accept-Language, Content-Language, Content-Type ve tek aralıklı Range.
  • Content-Type değeri yalnızca application/x-www-form-urlencoded, multipart/form-data veya text/plain olmalı.
  • XMLHttpRequest kullanılıyorsa xhr.upload üzerine olay dinleyicisi eklenmemiş olmalı.
  • İstekte ReadableStream kullanılmamalı.

Bu kuralları küçük bir fonksiyona dökmek, hangi değişikliğin ön uçuşu tetiklediğini anlamayı kolaylaştırır. Aşağıdaki fonksiyon MDN listesinin sadeleştirilmiş bir modelidir; tarayıcıların başlık değerlerine uyguladığı ek kısıtları içermez:

javascript
const BASIT_YONTEM = ["GET", "HEAD", "POST"];
const BASIT_BASLIK = ["accept", "accept-language", "content-language", "content-type", "range"];
const BASIT_TUR = ["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"];

function onUcusGerekirMi(method, headers = {}) {
  if (!BASIT_YONTEM.includes(method.toUpperCase())) return true;
  for (const [ad, deger] of Object.entries(headers)) {
    const kucuk = ad.toLowerCase();
    if (!BASIT_BASLIK.includes(kucuk)) return true;
    if (kucuk === "content-type") {
      const tur = deger.split(";")[0].trim().toLowerCase();
      if (!BASIT_TUR.includes(tur)) return true;
    }
  }
  return false;
}

console.log(onUcusGerekirMi("GET")); // false
console.log(onUcusGerekirMi("POST", { "Content-Type": "text/plain;charset=UTF-8" })); // false
console.log(onUcusGerekirMi("POST", { "Content-Type": "application/json" })); // true
console.log(onUcusGerekirMi("GET", { Authorization: "Bearer abc" })); // true
console.log(onUcusGerekirMi("DELETE")); // true
console.log(onUcusGerekirMi("POST", { "X-PINGOTHER": "pingpong" })); // true

Sonuçlar pratikte çok önemli bir şeyi gösteriyor: JSON gövdeli bir POST, bir Authorization başlığı veya bir DELETE isteği başka kökene gidiyorsa tarayıcı önce bir ön uçuş isteği gönderir. Sunucu bu ön uçuşa doğru cevap vermezse asıl istek hiç gönderilmez.

Basit bir GET isteğinde tarayıcının gönderdiği ve sunucunun döndürdüğü başlıklar MDN örneğinde şöyledir (yalnızca ilgili satırlar):

http
GET /resources/public-data/ HTTP/1.1
Host: bar.other
Origin: https://foo.example

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-Type: application/xml

Origin başlığını siz eklemezsiniz; tarayıcı her kökenler arası istekte otomatik gönderir ve yalnızca şema, ana makine ve port bilgisini içerir, yol bilgisi taşımaz. Access-Control-Allow-Origin: * her kökenin okumasına izin verir. Yalnızca belirli bir kökene izin vermek için o kökeni yazarsınız: Access-Control-Allow-Origin: https://foo.example.

Ön uçuşlu (preflight) istekler

Basit olmayan bir istekte tarayıcı, asıl istekten önce aynı adrese OPTIONS yöntemiyle bir ön uçuş isteği gönderir. Bu istekte iki özel başlık bulunur: Access-Control-Request-Method asıl istekte kullanılacak yöntemi, Access-Control-Request-Headers ise kullanılacak özel başlıkları bildirir. MDN'deki örnek, text/xml gövdeli ve X-PINGOTHER başlıklı bir POST isteğidir:

javascript
const fetchPromise = fetch("https://bar.other/doc", {
  method: "POST",
  mode: "cors",
  headers: {
    "Content-Type": "text/xml",
    "X-PINGOTHER": "pingpong",
  },
  body: "<person><name>Arun</name></person>",
});

Bu kod çalıştığında ağda önce şu ön uçuş alışverişi görülür:

http
OPTIONS /doc HTTP/1.1
Host: bar.other
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,x-pingother

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin

Sunucu, Access-Control-Allow-Methods ile hangi yöntemlere, Access-Control-Allow-Headers ile hangi başlıklara izin verdiğini virgülle ayrılmış listeler halinde bildirir. Access-Control-Max-Age, ön uçuş sonucunun kaç saniye önbellekte tutulabileceğini söyler; başlık yoksa varsayılan 5 saniyedir ve her tarayıcının bu değer için kendi üst sınırı vardır. Ön uçuş onaylandıktan sonra tarayıcı asıl POST isteğini gönderir; Access-Control-Request-* başlıkları yalnızca OPTIONS isteğinde bulunur. OPTIONS yönteminin genel anlamı için HTTP istek ve yanıt yapısı yazısına bakabilirsiniz.

Kimlik bilgili istekler

Tarayıcılar kökenler arası fetch() ve XMLHttpRequest çağrılarında varsayılan olarak çerez veya HTTP kimlik doğrulama bilgisi göndermez. Göndermek için fetch()'te credentials: "include", XHR'de withCredentials = true ayarlanır. Bu durumda sunucunun yanıtında Access-Control-Allow-Credentials: true bulunmazsa tarayıcı yanıtı reddeder ve kodunuza vermez:

javascript
const request = new Request("https://bar.other/resources/credentialed-content/", {
  credentials: "include",
});
const response = await fetch(request);

Kimlik bilgili isteklerde yıldız joker karakteri kullanılamaz. Sunucu Access-Control-Allow-Origin için açık bir köken, Access-Control-Allow-Headers ve Access-Control-Allow-Methods için açık birer liste, Access-Control-Expose-Headers için de açık başlık adları yazmalıdır. Yanıtta Access-Control-Allow-Origin: * varsa tarayıcı erişimi engeller ve yanıttaki Set-Cookie da çerez oluşturmaz.

Ön uçuş isteklerinin kendisi hiçbir zaman kimlik bilgisi taşımaz; ancak ön uçuş yanıtında Access-Control-Allow-Credentials: true bulunmalıdır ki asıl istek kimlik bilgisiyle yapılabilsin. Ayrıca CORS yanıtlarında ayarlanan çerezler normal üçüncü taraf çerez politikalarına tabidir: kullanıcının tarayıcısı üçüncü taraf çerezleri reddediyorsa, sunucu izin verse bile çerez kaydedilmeyebilir. Çerez özelliklerinin teknik ayrıntıları document.cookie ve Set-Cookie yazısında, kimlik bilgili isteklerin oluşturduğu risk ise CSRF saldırısı yazısında ele alınıyor.

CORS yanıt başlıkları

BaşlıkNeredeGörevi
Access-Control-Allow-OriginHer CORS yanıtıTek bir köken veya * ile okuma izni verir
Access-Control-Allow-MethodsÖn uçuş yanıtıİzin verilen yöntemlerin listesi
Access-Control-Allow-HeadersÖn uçuş yanıtıAsıl istekte kullanılabilecek başlıklar
Access-Control-Allow-CredentialsÖn uçuş ve asıl yanıttrue ise kimlik bilgili yanıt koda açılır
Access-Control-Expose-HeadersAsıl yanıtGüvenli liste dışındaki başlıkları koda gösterir
Access-Control-Max-AgeÖn uçuş yanıtıÖn uçuş sonucunun saniye cinsinden önbellek süresi
Vary: OriginKökene göre değişen yanıtÖnbelleklere yanıtın Origin başlığına bağlı olduğunu bildirir

Varsayılan olarak koda açılan CORS güvenli yanıt başlıkları şunlardır: Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified ve Pragma. Bunların dışındakiler için Access-Control-Expose-Headers gerekir. Access-Control-Allow-Headers için de bir istisna vardır: Authorization başlığı yıldız jokerine dahil değildir, her zaman adıyla yazılmalıdır.

Sunucu tarafında izin listesi örneği

Birden fazla kökene izin vermek istediğinizde Access-Control-Allow-Origin başlığına bir liste yazamazsınız; başlık tek bir köken alır. Doğru yöntem, gelen Origin değerini bir izin listesiyle karşılaştırmak ve eşleşirse aynı değeri geri yazmaktır. Yanıt kökene göre değiştiği için Vary: Origin de eklenmelidir. Aşağıdaki mantık, Request ve Response nesneleriyle çalışan bir sunucu işleyicisi olarak yazıldı ve Node.js 24 ile denendi; tarayıcıdaki bir sayfada Origin başlığını siz oluşturamazsınız, bu değeri tarayıcı ekler:

javascript
const IZINLI = ["https://foo.example", "https://panel.example"];

function corsBasliklari(request) {
  const origin = request.headers.get("Origin");
  const h = new Headers({ Vary: "Origin" });
  if (origin && IZINLI.includes(origin)) {
    h.set("Access-Control-Allow-Origin", origin);
    h.set("Access-Control-Allow-Credentials", "true");
  }
  if (request.method === "OPTIONS") {
    h.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
    h.set("Access-Control-Allow-Headers", "Content-Type, X-PINGOTHER");
    h.set("Access-Control-Max-Age", "600");
    return new Response(null, { status: 204, headers: h });
  }
  return Response.json({ mesaj: "merhaba" }, { headers: h });
}

const onUcus = new Request("https://bar.other/doc", {
  method: "OPTIONS",
  headers: {
    Origin: "https://foo.example",
    "Access-Control-Request-Method": "POST",
    "Access-Control-Request-Headers": "content-type,x-pingother",
  },
});
const y1 = corsBasliklari(onUcus);
console.log(y1.status); // 204
for (const [ad, deger] of y1.headers) console.log(`${ad}: ${deger}`);
// access-control-allow-credentials: true
// access-control-allow-headers: Content-Type, X-PINGOTHER
// access-control-allow-methods: GET, POST, PUT, DELETE
// access-control-allow-origin: https://foo.example
// access-control-max-age: 600
// vary: Origin

const yabanci = new Request("https://bar.other/doc", { headers: { Origin: "https://kotu.example" } });
const y2 = corsBasliklari(yabanci);
console.log(y2.status, y2.headers.get("access-control-allow-origin")); // 200 null

İzin listesinde olmayan kökene de 200 yanıtı gidiyor; ancak Access-Control-Allow-Origin başlığı olmadığı için tarayıcı bu yanıtı o kökendeki koda göstermez. Hassas işlemler için yine sunucu tarafında oturum ve yetki kontrolü yapmanız gerektiğini unutmayın.

Güvenli CORS yapılandırması için öneriler

MDN'nin uygulama rehberi şu ilkeleri önerir: Access-Control-Allow-Origin başlığı yalnızca gerçekten kökenler arası erişim gereken kaynaklarda bulunmalı ve mümkün olan en az sayıda kökene izin vermelidir. Örneğin aynı sunucu hem web sitesini hem de dışarıya açık bir API'yi barındırıyorsa başlığı yalnızca API yanıtlarına ekleyin. Kimlik bilgili erişim gerekiyorsa gelen Origin değerini körü körüne geri yansıtmayın; yalnızca bilinen kökenlere izin verin. Herkese açık ve kimlik bilgisi gerektirmeyen bir kaynak için * kullanıp Access-Control-Allow-Credentials başlığını hiç göndermeyin. Bunların hiçbiri gerekmiyorsa iki başlığı da göndermeyin.

Access-Control-Allow-Origin: null değerinden de kaçının. data: ve file: gibi şemalardan yüklenen belgeler ile sandbox içindeki belgelerin kökeni null olarak serileştirilir ve herhangi bir site böyle bir belge üretebilir; yani bu değer pratikte herkese izin vermek anlamına gelebilir.

Sık yapılan hatalar

Sık yapılan hatalar: CORS'u istemcide çözmeye çalışmak, Birden fazla kökeni tek başlığa yazmak, Kimlik bilgili istekte * kullanmak, OPTIONS isteğini yanıtlamamak
  • CORS'u istemcide çözmeye çalışmak: izin sunucu başlıklarıyla verilir; istemci tarafında Access-Control-Allow-Origin başlığı eklemek işe yaramaz.
  • Birden fazla kökeni tek başlığa yazmak: başlık tek bir köken veya * alır; izin listesini sunucu kodunda uygulayın.
  • Kimlik bilgili istekte * kullanmak: tarayıcı yanıtı engeller.
  • OPTIONS isteğini yanıtlamamak: ön uçuş başarısız olursa asıl istek gönderilmez.
  • Vary: Origin eklememek: kökene göre değişen yanıt önbelleklerde yanlış kökene sunulabilir.
  • Protokolü unutmak: https://foo.example ile http://foo.example farklı kökenlerdir.
  • Hata ayrıntısını koddan beklemek: güvenlik nedeniyle CORS hatasının sebebi JavaScript'e bildirilmez; yalnızca tarayıcı konsolunda görünür.

Sık Sorulan Sorular

CORS hatası alıyorum ama istek sunucuya ulaşıyor, neden?

Basit isteklerde tarayıcı isteği gönderir, sunucu da işler; ancak yanıtta uygun Access-Control-Allow-Origin başlığı yoksa yanıt kodunuza teslim edilmez. Bu yüzden sunucu günlüklerinde isteği görmeniz CORS'un sorunsuz olduğu anlamına gelmez.

Access-Control-Allow-Origin: * güvenli mi?

Herkese açık ve kimlik bilgisi gerektirmeyen kaynaklar (örneğin genel bir JavaScript kütüphanesi veya açık bir API) için uygundur. MDN, özel API'lerde joker karakter kullanmanın kötü bir fikir olduğunu açıkça belirtir; ayrıca joker karakter kimlik bilgili isteklerle zaten çalışmaz.

Aynı alan adının alt alan adları aynı köken sayılır mı?

Hayır. Köken, ana makine adının tamamını içerir; api.ornek.com ile www.ornek.com farklı kökenlerdir. Aralarındaki istekler için de CORS başlıkları gerekir.

Ön uçuş isteğini nasıl azaltabilirim?

İsteği basit istek koşullarına uydurabilirsiniz (yalnızca GET, HEAD veya POST ve güvenli listedeki başlıklar). Bu mümkün değilse sunucu Access-Control-Max-Age ile ön uçuş sonucunun bir süre önbellekte tutulmasına izin verebilir.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Web API öğrenme rehberi · CORS hataları ve çözümleri · Fetch API kullanımı · Request, Response ve Headers · CSRF saldırısı · Set-Cookie özellikleri