Görsellerde CORS Sorunu: Neden Çıkar, Nasıl Çözülür

Kilitli bulanık kare ve yanında net aynı kare

Canvas veya Web Workers üzerinden görsel işleme yapan bir projede, görsel farklı bir domain'den yüklenirse tarayıcı sessizce durmaz; yerine konsola SecurityError: The operation is insecure veya Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported mesajı düşer ve canvas içeriğinin tamamı okunamaz hale gelir. Sayfanın görsel olarak kusursuz göründüğü, görselin ekranda render edildiği, ancak getImageData() veya toDataURL() gibi yöntemlerin hata fırlattığı bu durum, ilk karşılaşıldığında görselin kendisinde bir bozukluk olduğu sanılır; oysa sorun görselde değil, origin'ler arasındaki veri akışının tarayıcı tarafından nasıl değerlendirildiğindedir.

CORS (Cross-Origin Resource Sharing), tarayıcının farklı kaynaklardan gelen içeriklere nasıl davranacağını belirleyen bir HTTP mekanizmasıdır. Normal bir img etiketi, farklı domain'den görsel yükleyip sayfada gösterebilir; tarayıcı bunu engellemez. Engelleme, ancak o görselin piksel verisini okumaya çalıştığınızda, yani JavaScript içeriğe erişmeye kalktığında devreye girer. Gösterme ve okuma iki ayrı izin katmanıdır ve bu ayrım, hatanın neden görsel eklenince değil de canvas işlemi yazılınca ortaya çıktığını açıklar.

Hatayı doğru yerde aramak için üç noktayı birden kontrol etmek gerekir: sunucunun CORS başlığı dönüp dönmediği, img etiketinin ya da Image nesnesinin crossorigin niteliğiyle yapılandırılıp yapılandırılmadığı ve önbelleğin eski bir yanıtı saklayıp saklamadığı. Bu üçlüden herhangi biri eksik kaldığında aynı hata ortaya çıkar; dolayısıyla sunucuyu düzelttikten sonra hata devam ediyorsa, liste bitmemiş demektir.

Tarayıcının aynı kaynak politikası ve görseller

Same-Origin Policy (aynı kaynak politikası), A sitesinin B sitesinin verilerine JavaScript aracılığıyla erişmesini engelleyen temel güvenlik katmanıdır. Tarayıcı bu politikayı uygularken protokol, domain ve port üçlüsüne bakar: https://example.com ile https://img.example.com bile birbirinden farklı origin'dir; subdomain farkı yeterlidir. http://example.com ile https://example.com de farklıdır; yalnızca protokol değişikliği origin'i değiştirir.

Görseller bu politikadan kısmen muaftır. Bir <img src="https://cdn.example.com/photo.jpg"> etiketi, aynı kaynak kısıtlaması olmadan yüklenip sayfada görüntülenebilir. Tarayıcı görseli indirir, render eder, kullanıcı ekranda görür; herhangi bir hata oluşmaz. Ancak JavaScript o görselin piksel verisine erişmeye çalıştığında, örneğin görseli bir canvas'a çizip getImageData() ile renk değerlerini okumaya kalktığında, tarayıcı bu isteği farklı değerlendirir.

Canvas bu noktada "kirlenmiş" (tainted) olarak işaretlenir. Kirli bir canvas'tan toDataURL(), toBlob() veya getImageData() çağrısı yapılamaz; yapılırsa SecurityError fırlatılır. Görsel sayfada görünmeye devam eder, canvas çizim işlemi tamamlanmış görünür; sorun yalnızca piksel okuma aşamasında kendini belli eder.

Canvas piksel okumasını neden engelliyor

Kısıtlamanın ardında soyut bir kural değil, somut bir saldırı senaryosu vardır. Kullanıcı, A sitesini ziyaret ederken aynı tarayıcı oturumunda B bankasına giriş yapmış olabilir. A sitesi bir canvas oluşturur, B bankasının favicon'ını ya da oturum açmış kullanıcıya özel bir küçük profil görselini canvas'a çizer ve getImageData() ile piksel değerlerini okur. Görselin piksel örüntüsünden kullanıcının o bankada hesabı olup olmadığı, hangi arayüz versiyonunu gördüğü veya oturumun aktif olup olmadığı çıkarılabilir. Tarayıcılar bu kanalı kapatmak için canvas piksel okumalarını origin kontrolüne bağlamıştır.

Pratik sonuç bellidir. drawImage() çağrısı hata vermez; görsel canvas'a sorunsuz çizilir. Hata, piksel verisine erişmeye kalktığınız anda fırlar; bu gecikmiş hata, zaman zaman sorunun canvas çizim kısmında değil okuma aşamasında olduğunu gizler ve yanlış yönde arama yapılmasına neden olur.

Yalnızca canvas değil. WebGL dokuları (texImage2D ile yüklenen görseller), OffscreenCanvas içinde işlenen görseller, createImageBitmap() çağrısına geçirilen cross-origin kaynaklar ve WebCodecs API'si ile işlenen veriler de aynı kısıtlamaya tabidir. Ortak nokta şudur: tarayıcı API'si piksel verisine erişiyorsa, CORS yapılandırması zorunlu hale gelir.

crossorigin niteliği: img etiketine ne eklenmeli

Sorunun iki ucunu birden kapatmak gerekir: img etiketi tarafı ve sunucu tarafı. Tarayıcıya "bu görseli cross-origin istek olarak indir" demek için img etiketine crossorigin niteliği eklenir:

<img crossorigin="anonymous" src="https://cdn.example.com/photo.jpg" id="sourceImage">

crossorigin="anonymous" değeri, tarayıcıya görseli çekerken credentials, yani cookie ve HTTP kimlik doğrulama bilgilerini göndermemesini söyler. Sunucu bu isteğe Access-Control-Allow-Origin: * başlığıyla yanıt verirse, canvas o görseli piksel seviyesinde okuyabilir.

crossorigin="use-credentials" ise cookie dahil kimlik bilgileriyle istek atar. Bu değerin çalışması için sunucunun Access-Control-Allow-Origin başlığında wildcard (*) yerine spesifik bir origin belirtmesi ve yanıta Access-Control-Allow-Credentials: true eklemesi gerekir. Public CDN'lerin büyük çoğunluğu bunu desteklemez; bu değer genellikle oturum açmış kullanıcılara özel içerik sunan sistemler içindir.

JavaScript ile dinamik görsel yüklemede aynı yapılandırma Image nesnesi üzerinden uygulanır:

const img = new Image();
img.crossOrigin = 'anonymous';
img.src = 'https://cdn.example.com/photo.jpg';
img.onload = () => {
  ctx.drawImage(img, 0, 0);
  const data = ctx.getImageData(0, 0, img.width, img.height); // artık çalışır
};

Sıralama kritiktir. crossOrigin özelliği atanmadan img.src belirlenirse, tarayıcı görseli CORS başlığı olmayan standart bir istek olarak indirir; sonradan crossOrigin ekleyip src'yi değiştirmemek yetmez, nitelik istek atılmadan önce yerinde olmalıdır. Aynı src'yi tekrar atamak önbelleğe alınmış yanıtı geçersiz kılmak için gereklidir; ama bu durumda bile tarayıcı önbellek kaydı karışıklık yaratabilir; bunu "Önbellek tuzağı" bölümünde ele alıyoruz.

Sunucu tarafında CORS başlığı nasıl eklenir

img etiketi doğru yapılandırılmış olsa bile, sunucu yanıtında Access-Control-Allow-Origin başlığı yoksa tarayıcı canvas'ı temiz saymaz. İki taraf da hazır olmak zorundadır.

Apache için .htaccess dosyasına veya site konfigürasyonuna şu blok eklenir:

<FilesMatch "\.(jpg|jpeg|png|gif|webp|svg|avif)$">
  Header set Access-Control-Allow-Origin "*"
</FilesMatch>

mod_headers modülünün etkin olması gerekir; etkin değilse a2enmod headers komutuyla açılabilir. Yalnızca görsel uzantılarını hedef alan bu blok, diğer kaynaklara gereksiz yere wildcard açmaktan kaçınır.

Nginx için benzer bir kısıtlama:

location ~* \.(jpg|jpeg|png|gif|webp|svg|avif)$ {
  add_header Access-Control-Allow-Origin "*";
}

Node.js / Express ile görsel serve ediyorsanız, middleware olarak başlık eklenebilir:

app.use('/images', (req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  next();
}, express.static('public/images'));

Wildcard yerine belirli origin'lere kısıtlamak istediğinizde, * yerine https://yoursite.com yazabilirsiniz. Birden fazla domain'e izin vermek için dinamik origin kontrolü gerekir:

const allowed = ['https://site1.com', 'https://site2.com'];
app.use('/images', (req, res, next) => {
  const origin = req.headers.origin;
  if (allowed.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Vary', 'Origin');
  }
  next();
});

Vary: Origin başlığı burada kritik rol oynar. Yanıtın origin'e göre farklılaştığını hem tarayıcı önbelleğine hem de CDN katmanına söyler; bu başlık olmadan, önbellek aynı URL için farklı origin'lere aynı yanıtı sunabilir ve bazı istekler CORS başlığı olmadan yanıt alır.

CDN katmanında CORS yapılandırması

Görseller doğrudan kendi sunucunuzdan değil bir CDN üzerinden sunuluyorsa, CORS başlığını CDN'e de taşımak gerekir. Bazı CDN'ler origin sunucudan gelen başlıkları otomatik iletir; bazıları ise edge konumunda önbelleğe aldığı ilk yanıtı sonraki tüm istekler için kullanır ve origin sunucuda sonradan yapılan başlık değişikliği CDN önbelleği geçersiz kılınana kadar etkisiz kalır.

Cloudflare kullananlar için en güvenilir yaklaşım, origin sunucunun doğru başlığı dönmesini sağlamak ve Cloudflare'in bunu iletmesine izin vermektir. "Cache Everything" kuralıyla önbelleğe alınmış ilk yanıt CORS başlığı içermiyorsa, önbelleği temizleyip tekrar test etmek gerekir. Transform Rules ile edge'de başlık eklemek de mümkündür; ancak bu, origin tarafında eksik olan başlığı CDN'de yamalar; asıl kaynağı düzeltmek tercih edilmelidir.

AWS CloudFront'ta yalnızca başlık eklemek yetmez; dağıtım yapılandırmasında Origin, Access-Control-Request-Headers ve Access-Control-Request-Method başlıklarını "cache key and origin request policy" kapsamına almak gerekir. Bu başlıklar origin'e iletilmezse, origin hangi domain'in istekte bulunduğunu bilemez ve uygun Access-Control-Allow-Origin değerini dönemez.

Yapılandırmayı doğrulamak için şu komut yeterlidir:

curl -H "Origin: https://yoursite.com" -I https://cdn.example.com/photo.jpg

Yanıtta access-control-allow-origin satırı varsa tarayıcı da aynı başlığı görecektir. Yoksa, sunucu veya CDN katmanında bir ayar eksik demektir.

Önbellek tuzağı: başlık ekledikten sonra hata devam ediyorsa

Sunucuya CORS başlığı eklenmiş, img etiketine crossorigin niteliği konulmuş, ancak hata hâlâ devam ediyor. Bu durumun en yaygın nedeni önbellektir. Tarayıcı görseli CORS başlığı olmadan zaten önbelleğe almışsa, sunucu artık doğru başlığı dönse bile tarayıcı eski önbellek kaydını kullanır.

Hızlı test için sayfayı zorla yenileyin: Windows'ta Ctrl+Shift+R, Mac'te Cmd+Shift+R. Kalıcı çözüm için görsel URL'lerine versiyon query'si eklemek önbelleği geçersiz kılar: photo.jpg?v=2. Daha doğru bir yaklaşım: crossorigin niteliği eklenmiş bir istek, tarayıcı önbelleğinde niteliksiz istekten ayrı bir giriş olarak saklanır; başlığı ekledikten sonra testi yeni bir gizli sekmede yapmak, tarayıcı önbelleğinin tabloya karışmasını önler.

CDN önbelleği için Vary: Origin başlığının origin sunucudan dönmesi şarttır. Bu başlık eksikse, CDN aynı URL için CORS başlığı olmadan önbelleğe aldığı yanıtı farklı origin'den gelen isteklere de sunabilir; origin sunucusunda başlık olduğu halde bazı kullanıcılar CORS başlığı almaz. CDN önbelleğini temizlemek ve Vary: Origin ile yeniden ısıtmak sorunu çözer.

Bir de önbellek ile crossorigin niteliği arasındaki etkileşim vardır. Tarayıcı, crossorigin="anonymous" ile yapılan isteği ve niteliksiz yapılan isteği aynı önbellek girdisi altında saklamaz; ayrı girişlerdir. Ancak görsel daha önce niteliksiz indirilmişse ve tarayıcı bu kaydı geçerli sayıyorsa, yeni crossorigin nitelikli isteğin yeni bir HTTP isteği başlatması gerekir. Bunun için ya URL'yi değiştirmek ya da önbelleği temizlemek gerekir.

Bu yapılandırma ne zaman gereksiz, ne zaman ters etki yaratır

Canvas veya JavaScript piksel verisine erişmediği her durumda CORS başlığı görsel servisi için zorunlu değildir. Bir img etiketi yalnızca sayfada görüntü oluşturuyorsa, srcset veya CSS background-image olarak kullanılıyorsa, link rel="preload" ile ön yükleniyorsa, tüm bu senaryolarda CORS başlığı gerekmez ve eksikliği "tainted canvas" hatasını tetiklemez.

Three.js veya benzeri WebGL kütüphaneleri ile doku yüklerken, OffscreenCanvas ve web worker içinde görsel işlerken ya da createImageBitmap() çağrısında cross-origin kaynak kullanırken durum değişir. Bu API'lerin tamamı piksel verisine eriştiğinden CORS yapılandırması zorunludur. Server-Side Rendering (SSR) veya Node.js ortamında görseli işlemek ise bu kısıtlamadan tamamen muaftır; tarayıcı güvenlik modeli sunucu tarafında uygulanmaz.

Ters etki senaryosuna gelince: wildcard Access-Control-Allow-Origin: * eklemek, görselin herkesin canvas'ında piksel seviyesinde işlenebileceği anlamına gelir. Kamuya açık görseller için bu genellikle sorun değildir. Ancak oturum açmış kullanıcıya özel içerik sunan bir servis, bu başlığı dikkatli eklemelidir; use-credentials akışında ise wildcard hiç kullanılamaz, spesifik origin ve Vary: Origin zorunludur.

Görseli sunucunuzla aynı origin'den sunmak, yani CDN yerine kendi domain'inizden servis etmek, CORS sorununu tümüyle ortadan kaldırır. Bant genişliği maliyeti veya performans kaygısıyla bunu her zaman yapamazsınız; ancak mümkünse en temiz çözümdür, ek başlık veya nitelik gerektirmez. Farklı origin zorunluysa, sunucu başlığı ve crossorigin niteliği birlikte çalışmak zorundadır; yalnızca birini eklemek yetmez.

Hatanın tamamlanmış göründüğü bir projede birdenbire ortaya çıkması da mümkündür: görsel kaynağı değiştiğinde, CDN'e geçildiğinde veya mevcut görsele JavaScript ile piksel işleme eklediğinizde. Yapılandırma, görsel eklenirken değil onu okumaya kalktığınızda test edilmesi gereken bir katmandır.

Geliştirici araçlarının Network sekmesini açın, görselin HTTP isteğine tıklayın ve yanıt başlıklarında access-control-allow-origin satırını arayın. Buradaki yanıt, tarayıcının gerçekte ne aldığını gösterir; sunucu yapılandırması, CDN önbelleği ve istek başlıkları bu sonuca birlikte katkıda bulunur. Satır oradaysa ve crossorigin niteliği de doğruysa, canvas hatası çözülmüş demektir.