Geçen ayki güvenlik incelemelerimizden birinde, müşterinin kıdemli geliştiricisi gülümseyerek "Biz React kullanıyoruz, XSS bize işlemez" dedi. İki saat sonra, sitenin blog bölümündeki zengin metin editöründen giren bir içerikle admin panelinde kendi yazdığımız script'i çalıştırıyorduk. Kimse kötü geliştirici olduğu için değil — ekip gayet yetkindi — framework'ün verdiği güvenlik hissinin sınırları bilinmediği için.
Bu cümleyi yılda belki on kez duyuyoruz ve neredeyse her seferinde aksini gösteren bir vaka çıkıyor. XSS ve CSRF, web'in iki emektar saldırısı, modern stack'lerde ölmedi; şekil değiştirdi. İkisini de bir kez hakkıyla anlamak, yıllarca sürecek bir refleks kazandırıyor.
XSS: sizin alan adınızda başkasının kodu
Cross-site scripting'in özü tek cümle: saldırgan, kullanıcı girdisi üzerinden sayfanıza JavaScript sokmayı başarır. O andan itibaren script, kurbanın oturumuyla ve sizin alan adınızın bütün itibarıyla çalışır — çerez okur, kurbanın adına işlem yapar, sayfa içeriğini değiştirir, girdiği her tuşu dinleyebilir. Tarayıcı açısından ortada şüpheli bir durum yoktur; kod sizin sitenizden gelmektedir.
Saldırının en tehlikeli türü, saklanan (stored) XSS: zararlı içerik veritabanınıza bir kez yazılır ve o sayfayı açan herkeste tekrar tekrar çalışır. Yorum kutusu, profil alanı, ürün açıklaması — kullanıcı içeriğinin saklanıp başkalarına gösterildiği her nokta adaydır. Link üzerinden tek kurbana işleyen yansıtılmış (reflected) türe göre etki alanı kıyas kabul etmez; girişteki blog vakamız da tam olarak buydu: içerik bir kez kaydedildi, paneli açan her yönetici için çalıştı.
Modern framework'lerin hakkını yiyelim mi? Yemeyelim: React, Vue ve benzerleri değişkenleri şablona basarken varsayılan olarak kaçışlar (escape), bu sayede saf XSS vakalarının büyük kısmı doğmadan ölür. Sorun, kaçış noktalarının belli ve bilindik olması: React'te dangerouslySetInnerHTML, vanilya tarafta innerHTML, href alanına yerleştirilen javascript: linkleri ve en klasiği — sunucudan gelen "güvenilir" HTML. Adında "dangerously" yazan fonksiyonun projede kırk yerde geçmesi, bize kod incelemesinde kırmızı alarm demektir.
Zengin metin editörü kullanan her uygulama bu riski yapısal olarak taşır; kullanıcıdan HTML alıyorsunuz ve bir gün onu bir sayfaya basacaksınız. Kural net: temizlik (sanitize) sunucuda, kaydetme anında ya da basma anında, denenmiş bir kütüphaneyle yapılır. İstemci tarafındaki temizlik kozmetiktir — saldırgan sizin arayüzünüzü kullanmak zorunda değil, API'nize doğrudan istek atar. Üstüne Content-Security-Policy başlığıyla script kaynaklarını kilitlemek, bir şey kaçtığında hasarı sınırlayan ikinci kemerdir.
CSP demişken pratik bir tavsiye: sıkı bir politikayı doğrudan devreye almak, meşru script'leri de kırıp paniğe yol açabilir. Önce report-only modunda çalıştırın; tarayıcılar ihlalleri engellemez ama size raporlar. Bir-iki hafta rapor toplayıp politikayı gerçeğe göre ayarlar, sonra zorlayıcı moda geçersiniz. Bir de üçüncü parti script'ler meselesi var: sayfanıza eklediğiniz her analitik ve pazarlama aracı, sizin güvenlik çeperinizin içinde çalışan yabancı koddur — envanterini tutmayan ekip, neyi savunduğunu bilmiyordur.
CSRF: kurbanın tarayıcısına iş yaptırmak
CSRF'in mantığı daha da kurnaz, çünkü saldırgan sitenize hiç dokunmaz. Kullanıcı sizin uygulamanızda oturum açıkken kötü niyetli bir sayfaya girerse, o sayfa kullanıcının tarayıcısına sizin API'nize istek attırabilir. Tarayıcı, ilgili çerezleri isteğe otomatik ekler; sunucunuz açısından istek tamamen meşru görünür. Kullanıcı bir haber sitesinde sanırken arka planda "para transferi" ya da "e-posta değiştir" isteği gitmiştir.
Savunma katmanlı kurulur. Çerezlere SameSite=Lax ya da Strict vermek, başka sitelerden tetiklenen isteklerde çerezin gönderilmesini keser ve saldırının belkemiğini kırar. Durum değiştiren her isteğe — para, veri, ayar, ne olursa — sunucunun ürettiği bir CSRF token'ı eklemek, isteğin gerçekten sizin arayüzünüzden geldiğini kanıtlar. Şifre değişikliği, transfer onayı gibi hassas işlemlerde ise kullanıcıya parolasını ya da ikinci faktörü yeniden sormak, en kötü senaryonun bile hasarını sınırlar.
Bu saldırının gerçek hayattaki yüzünü bir denetimde gördük: bir yönetim panelinde kullanıcı silme işlemi GET isteğiyle, token'sız çalışıyordu. Hazırladığımız zararsız kanıt sayfası, paneli açık unutan bir yöneticinin tarayıcısına — yönetici hiçbir şeye tıklamadan — test kullanıcısını sildirtti. Ekibin yüzündeki ifadeyi hâlâ hatırlıyorum; "teorik risk" ile "gözünün önünde silinen kayıt" arasındaki mesafe, hiçbir sunumla kapatılamayacak kadar öğretici.
SameSite geldi, token emekli olmadı
Tarayıcıların SameSite varsayılanlarını sıkılaştırması işimizi gerçekten kolaylaştırdı; bunu teslim edelim. Ama "artık token'a gerek yok" çıkarımı tehlikeli. Eski tarayıcılar sahada yaşamaya devam ediyor, alt alan adları arasındaki güven ilişkileri beklenmedik kapılar açıyor ve Lax modunun üst seviye GET isteklerine izin verdiğini unutan ekipler, durum değiştiren işlemi GET ile yaparak kapıyı kendi elleriyle aralıyor. (GET ile kayıt silen endpoint gördük; link önizlemesi yapan bir sohbet uygulaması, sadece linki görüntüleyerek kayıtları silmişti. Gerçek hikâye.)
Savunmanın hiçbir katmanı tek başına yeterli değil; zaten katman olmalarının sebebi bu.
Kod incelemesinde aradığımız refleksler
Bu iki saldırı sınıfına karşı bağışıklık, araç meselesinden çok refleks meselesi. Bizim incelemelerde aradığımız zincir şu: girdi sunucuda doğrulanıyor mu; çıktı, basıldığı bağlama göre kaçışlanıyor mu; HTML'e ham veri basan bir yol var mı; CSP başlığı script kaynaklarını kilitliyor mu; çerezlerde SameSite, HttpOnly ve Secure üçlüsü tam mı; durum değiştiren her uçta token var mı. Bu zinciri ekipçe refleks hâline getiren projelerde, bu iki açık sınıfını denetimlerde artık görmüyoruz.
Refleksleri insan hafızasına emanet etmeyin; otomasyona bağlayın. Statik analiz kuralları tehlikeli fonksiyon kullanımlarını daha commit aşamasında işaretleyebilir; bağımlılık taraması, kullandığınız paketlerdeki bilinen açıkları CI hattında yakalar; uçtan uca test setinize eklenecek birkaç zararsız saldırı senaryosu, koruma katmanlarından biri gevşediğinde alarmı çalar. Framework ve kütüphaneleri güncel tutmak da bu hijyenin parçası — savunmaların çoğunu sizin yerinize onlar taşıyor, eski sürümde kalmak o savunmalardan feragat etmek demek.
"Bizim ürün mobil uygulama, web'imiz yok" diyenlere de bir hatırlatma: neredeyse her mobil uygulamanın arkasında bir yönetim paneli var ve o panel web. Saldırganın müşteri uygulamanızla uğraşmasına gerek yok; içeriden her şeyi yönetebildiği panel dururken niye uğraşsın? Denetimlerimizde en zayıf halka çoğunlukla o gösterişsiz, "sadece biz kullanıyoruz" denilen panel çıkıyor — çünkü kimse onu ürünün parçası saymıyor, saldırgan hariç.
Kendi uygulamanızın bu zincirin neresinde durduğundan emin değilseniz, bir güvenlik incelemesi için yazın. "React kullanıyoruz" cümlesinden iki saat sonrasını, müşteri olarak değil okur olarak deneyimlemek her zaman daha ucuz.