Same-origin politikası ve CORS'un gevşettiği şey
Origin üç parçanın birleşimidir: şema, host ve port. https://shop.example.com ile https://api.example.com farklı origin'lerdir; http://example.com ile https://example.com de öyle. Yol (path) hesaba katılmaz.
Tarayıcılar same-origin politikası uygular: bir origin'deki JavaScript başka bir origin'e istek gönderebilir, ama karşı taraf izin vermedikçe yanıtı okuyamaz. Aynı kural başka bir host'tan yüklenen web fontlarını ve canvas'a çizilmiş cross-origin bir görselin piksellerini geri okumayı da kapsar.
İzin gerektirmeden serbest kalan çok şey var. Bir sayfa her yerden görsel, stil dosyası ya da script gömebilir; bir form her siteye POST edebilir. Politikanın koruduğu şey okumaktır: o olmasa, açtığınız herhangi bir sayfa çerezlerinizle webmail'inizi ya da bankanızın API'sini sessizce çağırıp yanıtı okuyabilirdi.
CORS (Cross-Origin Resource Sharing) bu izni verme yoludur. Tarayıcı hangi origin'in sorduğunu Origin istek başlığıyla sunucuya bildirir, sunucu da yanıtı hangi origin'lerin okuyabileceğini Access-Control-* başlıklarıyla söyler. CORS'ta hiçbir şey isteği sunucuda durdurmaz: yanıtı sayfadan saklayan tarayıcıdır.
Basit istekler ve preflight istekleri
Basit istekler doğrudan gider. Bir GET, HEAD ya da POST, yalnızca Accept, Accept-Language, Content-Language gibi CORS-safelisted başlıklar taşıyorsa ve gövdesinin Content-Type'ı text/plain, multipart/form-data ya da application/x-www-form-urlencoded ise basit sayılır. Tarayıcı onu Origin başlığıyla gönderir, yanıtı alır ve sayfaya ancak Access-Control-Allow-Origin o origin'e izin veriyorsa verir.
Geri kalan her istekten önce bir preflight gider: asıl istek gönderilmeden izin isteyen bir OPTIONS isteği. Metodu Access-Control-Request-Method, diğer başlıkları Access-Control-Request-Headers içinde bildirir. Sunucu 2xx durum koduyla, uygun Access-Control-Allow-* başlıklarıyla ve yönlendirme yapmadan cevap vermelidir. Tarayıcı asıl isteği ancak o zaman gönderir.
En sık şaşırtan nokta, JSON API'lerinin neredeyse her çağrıda preflight tetiklemesidir. Content-Type: application/json listede yoktur, Authorization da yoktur; bu yüzden bearer token ile JSON gönderen bir fetch, tarayıcı preflight cevabını önbelleğinde tutmuyorsa fazladan bir gidiş-dönüş öder. Access-Control-Max-Age tam olarak bunun içindir: Chromium bu cevabı en fazla iki saat, Firefox en fazla 24 saat tutar; başlık yoksa tarayıcı onu beş saniye saklar.
Bir preflight ve asıl isteğin önünü açan cevap
OPTIONS /v1/orders HTTP/1.1
Host: api.example.com
Origin: https://shop.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://shop.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 7200
Vary: Origin
Access-Control başlıkları tek tek
Access-Control-Allow-Origin kararı veren başlıktır. Tam olarak tek bir değer alır: her origin için *, https://shop.example.com gibi tek bir origin ya da null. Virgülle ayrılmış liste geçersizdir; null'a ise asla izin verilmemelidir, çünkü sandbox'lı iframe'ler ve yerel dosyalar da Origin: null gönderir.
Access-Control-Allow-Methods ve Access-Control-Allow-Headers preflight'a cevap verir: asıl istek hangi metotları ve hangi istek başlıklarını kullanabilir. Access-Control-Max-Age, tarayıcının bu cevabı kaç saniye yeniden kullanabileceğini söyler.
Access-Control-Allow-Credentials: true, çerez ya da HTTP kimlik doğrulamasıyla gönderilmiş bir isteğin yanıtını sayfanın okumasına izin verir. Beraberinde gelen kurallar bir sonraki bölümde.
Access-Control-Expose-Headers, sayfadaki JavaScript'in okuyabileceği yanıt başlıklarını listeler. Bu başlık yoksa script yalnızca Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified ve Pragma'yı görür; özel bir X-Request-Id ya da yükleme kütüphanelerinin ihtiyaç duyduğu ETag görünmez kalır.
İstek tarafında üç başlık var; üçünü de tarayıcı yazar ve sayfa kodu taklit edemez: Origin, Access-Control-Request-Method ve Access-Control-Request-Headers.
Credentials: çerezler, Authorization ve * neden çalışmaz
Çerez ya da HTTP kimlik doğrulaması taşıyan istek "credentials" içeren istektir: credentials: 'include' ile fetch ya da withCredentials = true ile XMLHttpRequest. Bu isteklerde tarayıcı her kuralı sıkılaştırır.
Access-Control-Allow-Origin tam origin'i yazmak zorundadır; * reddedilir. Access-Control-Allow-Credentials: true bulunmalıdır. Allow-Headers, Allow-Methods ya da Expose-Headers içindeki * artık joker değildir: adı gerçekten * olan bir başlık gibi okunur.
Bir yanıt yalnızca tek bir origin yazabildiği için, birden fazla ön yüzü olan sunucu bir izin listesi tutar, gelen Origin'i bu listeyle karşılaştırır ve eşleşeni geri yazar. Cevap artık bir istek başlığına bağlı olduğundan, hiç CORS başlığı taşımayanlar dahil her yanıt Vary: Origin da söylemelidir; yoksa bir önbellek bir origin'in cevabını başka birine verir.
Klasik hata, credentials'a izin verirken herhangi bir origin'i geri yazmaktır. Bu, internetteki her sitenin oturum açmış kullanıcılarınızın verisini onların kendi tarayıcıları üzerinden okumasına izin verir. Güvenlik tarayıcılarının ilk yakaladığı CORS hatası budur ve OWASP Top 10 içinde bozuk erişim kontrolü başlığına girer. Çerezlerin de kendi kapısı var: bir çerez cross-site istekte ancak SameSite=None ve Secure ile ayarlanmışsa gider.
Origin'i izin listesinden geri yazan örnek (Node.js, Express)
const allowed = new Set([
'https://shop.example.com',
'https://admin.example.com',
]);
app.use((req, res, next) => {
const origin = req.headers.origin;
res.vary('Origin');
if (allowed.has(origin)) {
res.set('Access-Control-Allow-Origin', origin);
res.set('Access-Control-Allow-Credentials', 'true');
}
if (req.method === 'OPTIONS') {
res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.set('Access-Control-Allow-Headers', 'Authorization, Content-Type');
res.set('Access-Control-Max-Age', '7200');
return res.sendStatus(204);
}
next();
});
Sık görülen CORS hataları ve anlamları
Konsol mesajı hangi kuralın bozulduğunu söyler. Mesajı Network sekmesi açıkken okuyun; asıl hikâyeyi çoğu zaman başarısız isteğin durum kodu anlatır. Aşağıdaki mesajlar Chromium'un ifadeleridir.
No 'Access-Control-Allow-Origin' header is present on the requested resource. Yanıt başlıksız geldi. Çoğu zaman yanıt bir hatadır: 404, 500, giriş sayfasına yönlendirme ya da bir güvenlik duvarının 403'ü; bunları üreten kod CORS işleyicinize hiç ulaşmaz. Önce durum kodunu düzeltin, sonra hata yanıtlarının da başlığı taşıdığından emin olun ki bir sonraki hata gerçek nedenini göstersin.
The 'Access-Control-Allow-Origin' header contains multiple values '*, *', but only one is allowed. Başlığı iki katman ekliyor: genellikle uygulama ve önündeki proxy ya da CDN. Yalnızca birini bırakın.
Response to preflight request doesn't pass access control check: It does not have HTTP ok status. OPTIONS isteği 401, 403, 404 ya da 405 aldı. Olağan neden CORS'tan önce çalışan kimlik doğrulamasıdır: preflight sizin Authorization başlığınızı hiçbir zaman taşımaz, bu yüzden her giriş kontrolünden önce cevaplanmalıdır.
Redirect is not allowed for a preflight request. URL yönlendiriyor: http'den https'e, eksik sondaki eğik çizgi, başka bir host. Doğrudan son URL'i çağırın.
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. Sayfa çerez gönderiyor, sunucu * diyor. Tam origin'i geri yazıp Access-Control-Allow-Credentials: true ekleyin ya da credentials göndermeyi bırakın.
Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response. Adı geçen başlığı Access-Control-Allow-Headers'a ekleyin.
null origin, sayfanın file:// ile açıldığını gösterir; sayfayı yerel bir web sunucusundan açın. Kaçınılması gereken bir "çözüm" de var: mode: 'no-cors' hatayı susturur ama kodunuza okuyamayacağı opak bir yanıt verir. İstek gider, script'iniz hiçbir şey alamaz.
CDN arkasında CORS: önbellek ve Vary: Origin
Bir önbellek her URL için tek bir yanıt saklar ve o URL'i isteyen herkese onu verir. İsteğe göre değişen CORS başlıkları bu varsayımı iki şekilde bozar.
Origin sunucu Access-Control-Allow-Origin'i yalnızca istekte Origin başlığı varsa ekliyorsa, neyin önbelleğe gireceğine ilk istek karar verir. İlk istek düz bir <img> etiketinden ya da doğrudan bir ziyaretten geldiyse önbellekteki kopyada CORS başlığı yoktur ve kopyanın süresi dolana kadar sonraki her cross-origin fetch başarısız olur. Hata rastgele görünür, çünkü kimin önce geldiğine bağlıdır.
Origin sunucu isteyen origin'i geri yazıyorsa, önbellekteki kopya tek bir origin'in adını taşır; aynı URL'i okuyan ikinci bir site başkasına yazılmış bir cevap alır.
İki temiz tasarım var. Fontlar, görseller, script'ler ve herkese açık JSON gibi dosyalarda herkese aynı başlığı gönderin: ya * ya da tek sitenizin origin'i. Böylece önbellekteki her kopya her ziyaretçi için doğrudur ve isabet oranı etkilenmez. Cevap origin'e göre değişmek zorundaysa, o URL'in her yanıtında Vary: Origin gönderin ki önbellek varyantları ayrı tutsun. Bunun isabet oranına bir bedeli vardır; yalnızca gereken yollarda kullanın.
Tarayıcılar da önbellekler. Önce düz bir <img> etiketiyle yüklenip sonra crossorigin özniteliğiyle istenen bir görsel, tarayıcı önbelleğinden başlıksız gelebilir. Aynı iki tasarım bu durumu da çözer.
CDN.com.tr'de CORS başlıkları
CDN üzerinden sunulan dosyalarda başlığı edge kendisi ekleyebilir. Panelde Yayınlama Kuralları'nı açın, yolu kapsayan kuralı düzenleyin (örneğin /fonts/ ya da /static/) ve Özel başlıklar alanına her satıra bir başlık gelecek şekilde Başlık-Adı değer yazın (sekmeli kural sayfasında: Başlıklar → Yanıta başlık ekle). Boşluk içeren değer çift tırnak içine yazılır.
Edge bu başlıkları başarılı ve yönlendirme yanıtlarına (2xx ve 3xx), önbellekten verilenler dahil, kural yayınlandığı andan itibaren ekler. Panel ve edge, başlık satırında ;, { ve } karakterlerini kabul etmez; CORS başlığının bunlara ihtiyacı yoktur, çünkü Access-Control-Allow-Origin tek bir değer alır.
Origin sunucunuz bu dosyalar için zaten bir Access-Control-* başlığı gönderiyorsa, aynı adı o kuralda Başlıkları Gizle listesinden seçin. Yoksa tarayıcı iki değer alır ve ikisini de reddeder.
İki iş origin sunucunuzda kalır. Birincisi preflight'tır: edge OPTIONS isteklerini sunucunuza iletir ve hiçbir zaman önbelleğe almaz; preflight gerektiren bir API bunlara kendisi cevap verir. Fontlar, görseller ve herkese açık JSON için yapılan düz GET istekleri preflight gerektirmez ve onlara kuraldaki başlık yeter. İkincisi origin'e göre değişen cevaplardır: credentials içeren isteklerin gerektirdiği şekilde izin verilen birkaç origin'den birini geri yazmak uygulamanızın kararıdır. Origin sunucunuz Vary: Origin gönderdiğinde, kural Vary'yi yok sayacak şekilde ayarlanmadıkça edge bu varyantları ayrı tutar. Görsel optimizasyonu açık bir kuralda JPEG ve PNG görseller kendi Vary başlığını yazan optimizer üzerinden sunulur; görsellere sabit bir başlık verin.
Site geneli WAF açıksa OPTIONS varsayılan olarak izinli metotlar arasındadır; PUT, PATCH ve DELETE için İzin verilen HTTP metotları listesinde işaret koymak gerekir. Edge'in reddettiği istek CORS başlığı olmayan bir 403 alır ve tarayıcı bunu CORS hatası olarak raporlar. O 403'teki X-MT-Blocked-By: cdn-edge-security başlığı, hayır diyenin uygulamanız değil edge olduğunu söyler.
/fonts/ ya da /static/ kuralında Özel başlıklar
Access-Control-Allow-Origin *
Access-Control-Expose-Headers "Content-Length, ETag"
Object storage bucket'larında CORS
Tarayıcının doğrudan bir bucket'a yüklediği dosyalara, örneğin bir web uygulamasından presigned PUT ile gönderilenlere, uygulamanız değil depolama servisi cevap verir; bu yüzden bucket'ın kendi CORS kuralları gerekir.
CDN.com.tr object storage bu kuralları standart S3 çağrısı PutBucketCors ile https://s3.cdn.com.tr üzerinden kabul eder ve tarayıcının preflight isteklerine kendisi cevap verir. Bucket CORS'u ayarlayabilen her S3 aracı çalışır; AWS CLI ile tek komut ve küçük bir JSON dosyası yeter.
Yazma yapan her şey için * yerine tam origin'leri listeleyin ve ETag'i dışa açın: tarayıcıdaki yükleme kütüphaneleri multipart yüklemeyi tamamlamak için onu okur. CORS bir bucket'ı herkese açık da yapmaz. Özel bir bucket anonim okumalara yine 403 döner; CORS yalnızca sayfanın, almasına izin verilmiş bir yanıtı okuyup okuyamayacağına karar verir.
AWS CLI ile bucket CORS
# cors.json
{
"CORSRules": [{
"AllowedOrigins": ["https://app.example.com"],
"AllowedMethods": ["GET", "PUT"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}]
}
aws s3api put-bucket-cors --bucket my-bucket \
--cors-configuration file://cors.json \
--endpoint-url https://s3.cdn.com.tr
aws s3api get-bucket-cors --bucket my-bucket \
--endpoint-url https://s3.cdn.com.tr
CORS'u komut satırından test etmek
curl CORS uygulamaz; bu yüzden bir sunucunun gerçekte ne söylediğini görmek için doğru araçtır. Origin başlığını kendiniz gönderin ve dönen Access-Control-* başlıklarını okuyun; preflight için, tarayıcının ekleyeceği iki başlıkla OPTIONS gönderin.
Üç şeye bakın: preflight'ta 2xx durum kodu, tam olarak bir Access-Control-Allow-Origin ve değerin origin'e göre değiştiği her yerde Vary: Origin. Sonra isteği CDN üzerinden tekrarlayıp karşılaştırın. CDN.com.tr'de X-Proxy-Cache-MT başlığı cevabın önbellekten gelip gelmediğini gösterir; önbellekten gelen cevap ilkinden farklıysa yukarıda anlatılan önbellek sorunu devrededir.
# a simple request: what does the edge answer for this origin?
curl -sI https://cdn.example.com/fonts/inter.woff2 \
-H 'Origin: https://www.example.com' \
| grep -i -E 'access-control|vary|x-proxy-cache'
# a preflight, as the browser would send it
curl -si -X OPTIONS https://api.example.com/v1/orders \
-H 'Origin: https://shop.example.com' \
-H 'Access-Control-Request-Method: PUT' \
-H 'Access-Control-Request-Headers: authorization, content-type'
CORS hakkında sık sorulanlar
CORS API'mi korur mu?
Hayır. CORS, kullanıcılarınızın tarayıcısını onlar adına veri okumaya çalışan sayfalardan korur. API'nizi doğrudan çağıran bir script'e, sunucuya ya da curl'e karşı hiçbir şey yapmaz; basit istekler, tarayıcı cevabı sonradan saklasa bile sunucunuza ulaşır. API'nizin kendi kimlik doğrulaması ve yetkilendirmesi yine gerekir.
Access-Control-Allow-Origin: * güvenli mi?
Giriş gerektirmeyen herkese açık kaynaklar için, yani fontlar, görseller, script'ler ve herkese açık JSON için evet: bunları zaten herkes indirebilir. Çerezlere ya da kullanıcının oturumuna dayanan her şey için yanlıştır; tarayıcılar onu credentials ile birlikte kabul etmez.
Birden fazla origin'e nasıl izin veririm?
Liste ile değil: başlık tek değer alır. Sunucuda bir izin listesi tutun, istekteki Origin listedeyse onu geri yazın ve önbellekler cevapları ayrı tutsun diye her yanıtta Vary: Origin gönderin.
İstek Postman'da ya da curl'de çalışıyor, tarayıcıda neden çalışmıyor?
Çünkü CORS'u yalnızca tarayıcılar uygular. Postman ve curl isteği gönderip ne dönerse gösterir; tarayıcı ise JavaScript'inize yanıtı göstermeden önce Access-Control-* başlıklarını kontrol eder.
CORS hatası neden yalnızca bazen çıkıyor?
Genellikle bir önbellek yüzünden. Yanıt Origin başlığına göre değişiyor ama Vary: Origin söylemiyorsa, önbelleğe ilk ulaşan istek diğer herkesin ne alacağına karar verir. Herkese açık dosyalarda sabit bir başlık, cevabın değişmesi gereken yerde Vary: Origin gönderin.
CDN.com.tr'de CORS'u nasıl açarım?
CDN'in sunduğu dosyalar için Access-Control-Allow-Origin'i dağıtım kuralının Özel başlıklar alanına ekleyin; API'nizin preflight cevapları ve origin'e göre değişen cevaplar origin sunucunuzdan gelir. Object storage bucket'ları için CORS kurallarını https://s3.cdn.com.tr üzerinde S3 PutBucketCors çağrısıyla ayarlayın.