Ders 14 / 27
Siteler Arası Betik Çalıştırma
Sayfaya giren metnin kod olarak çalışması sorunu; yansıyan, depolanmış ve DOM tabanlı türlerin oluşum yolu, bağlama duyarlı çıktı kaçırma ve bağ hedeflerinde şema izin listesi.
İçindekiler
Bu kursun ilk iki konusu kaliteyi ölçülebilir iki eksende ele aldı. Başarım konusu sayfanın ne kadar hızlı kullanılabilir olduğunu bütçeyle bağladı; erişilebilirlik konusu, Erişilebilirlik Testi dersinde otomatik denetimin nereye kadar gidebildiğini ve elle doğrulamanın nerede zorunlu olduğunu gösterdi. İki eksen de kullanıcının arayüzle kurduğu ilişkiyi iyileştirmeyi hedefliyordu.
Üçüncü eksen ters yönde durur: kullanıcıyı arayüzün kendisinden korumak. Kuzey Yamaç Ölçüm İstasyonu uygulamasında her ölçüm kaydına serbest metin bir ölçüm notu girilir. Bu not veritabanına yazılır, başka kullanıcıların ekranında görünür, dışa aktarılan raporda yer alır. Notu yazan kişi kötü niyetli olmasa bile, yazdığı metnin sayfaya nasıl girdiği bir güvenlik kararıdır. Bu ders, o metnin veri olarak kalmasını sağlayan katmanı kurar.
Metnin Koda Dönüştüğü An
Tarayıcı bir belgeyi ayrıştırırken metni bağlamına göre yorumlar. Web’in Temelleri ve HTML
kursunda kurulan ayrım burada belirleyici olur: < karakteri ayrıştırıcı için bir etiketin
başlangıcıdır, sıradan bir harf değildir. Uygulama, kullanıcıdan gelen bir dizgiyi belgeye
olduğu gibi yerleştirdiğinde ayrıştırıcıya “burada veri var” demez; ayrıştırıcı gördüğü
karakterleri kendi kurallarına göre okur ve dizginin içindeki işaretlemeyi yapı olarak
kabul eder.
Siteler arası betik çalıştırma (cross-site scripting), bu sınır kaymasının adıdır: bir
kaynağın (origin) belgesi içinde, o kaynağın yetkileriyle, uygulamanın yazmadığı kod
çalışır. Yetki sözcüğü kritiktir. Çalışan kod, belgeyle aynı kaynakta olduğu için o
kaynağın yerel depolamasını okuyabilir, HttpOnly işaretli olmayan çerezlerine erişebilir,
kullanıcının oturumuyla istek gönderebilir. Aynı kaynak politikası (same-origin policy) bu
kodu yabancı saymaz, çünkü kod belgenin içindedir.
Sorunun kökeni bir ayrıştırıcı hatası değildir. Kökeni, uygulamanın veriyi kod olarak yorumlanabilecek bir yere, yorumlanmasını engelleyen bir dönüşüm yapmadan koymasıdır.
Üç Oluşum Yolu
Aynı sonuca üç farklı yoldan varılır ve savunmanın nereye konacağı bu yola bağlıdır.
Yansıyan (reflected) biçimde, girdi isteğin kendisinden gelir ve aynı yanıtta geri yazılır. Ölçüm listesindeki arama alanı sorgu dizgisine (query string) yazılır, sunucu “aradığınız terim: …” başlığını üretirken bu değeri sayfaya koyar. Zarar tek bir yanıtta oluşur ve kurbanın hazırlanmış bir adresi açması gerekir.
Depolanmış (stored) biçimde, girdi bir kez kaydedilir ve sonraki her okumada yeniden sunulur. Ölçüm notu tam olarak bu sınıftadır: notu kaydeden kullanıcı ile etkilenen kullanıcı farklı kişilerdir ve etkilenen kişi sıradan bir gezinti yapar. Kalıcılığı nedeniyle etkisi en geniş türdür.
DOM tabanlı biçimde, veri sunucuya hiç uğramaz. İstemci kodu, adresin parçasını (fragment), yerel depolamadaki bir kaydı veya bir ileti olayının içeriğini okur ve belgeye yazar. Sunucu günlüklerinde iz kalmaz; savunma yalnızca istemci kodunda kurulabilir. Tarayıcı ve Web Platformu kursunda tanıtılan depolama API’leri bu yüzden güvenilir kaynak sayılmaz: yerel depolamaya yazan da aynı sayfada çalışan koddur.
Savunmanın Yeri: Girişte Değil Çıkışta
Sezgisel çözüm girdiyi süzmektir: kayıt sırasında tehlikeli görünen karakterleri atmak. Bu yaklaşım iki nedenle yetersizdir. Birincisi, hangi karakterin tehlikeli olduğu girdinin nereye yazılacağına bağlıdır ve kayıt anında bu bilinmez; aynı not hem HTML gövdesinde hem bir öznitelikte hem de dışa aktarılan bir adreste görünebilir. İkincisi, süzme veriyi bozar: “12’lik anemometre < 3 m/sn okudu” cümlesi geçerli bir ölçüm notudur ve olduğu gibi saklanmalıdır.
Doğru yer çıkıştır. Veri deposunda ham metin durur; belgeye yazılacağı anda, yazılacağı bağlamın kurallarına göre dönüştürülür. Bu dönüşüme kaçırma (escaping) denir: anlamlı karakterleri, o bağlamda anlamını yitiren ama görüntüsü korunan gösterimlerle değiştirmek.
Kaçırma bağlamdan bağımsız değildir. Dört ayrı bağlamın dört ayrı kuralı vardır ve birinin kuralını başka bir bağlamda uygulamak koruma sağlamaz.
Dört Bağlam, Dört Kural
Aşağıdaki dosya aynı ölçüm notunu dört bağlam için ayrı ayrı kaçırır. Not, dört bağlamın da anlamlı karakterlerini taşıyan zararsız bir işaret metnidir.
// kacirma.mjs — ayni olcum notunu dort baglamda kaciran islevler const GOVDE = { '&': '&', '<': '<', '>': '>' }; const OZNITELIK = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; function govdeKacir(metin) { return String(metin).replace(/[&<>]/g, (c) => GOVDE[c]); } function oznitelikKacir(metin) { return String(metin).replace(/[&<>"']/g, (c) => OZNITELIK[c]); } function adresKacir(metin) { return encodeURIComponent(String(metin)); } function betikKacir(metin) { return JSON.stringify(String(metin)) .replace(/</g, '\\u003c') .replace(/>/g, '\\u003e') .replace(/&/g, '\\u0026'); } const not = `12'lik <isaret> & "kuzey" yamaci`; const baglamlar = [ ['HTML govdesi', govdeKacir(not)], ['Oznitelik degeri', oznitelikKacir(not)], ['Adres bileseni', adresKacir(not)], ['Betik dizgisi', betikKacir(not)], ]; console.log('girdi:', not); for (const [ad, sonuc] of baglamlar) { console.log(ad.padEnd(18), sonuc); }
$ node kacirma.mjs girdi: 12'lik <isaret> & "kuzey" yamaci HTML govdesi 12'lik <isaret> & "kuzey" yamaci Oznitelik degeri 12'lik <isaret> & "kuzey" yamaci Adres bileseni 12'lik%20%3Cisaret%3E%20%26%20%22kuzey%22%20yamaci Betik dizgisi "12'lik \u003cisaret\u003e \u0026 \"kuzey\" yamaci"
Dört satır dört farklı çıktı verir ve farkların her biri bir kurala karşılık gelir.
Gövde bağlamında yalnız üç karakterin dönüştürülmesi yeter; tırnak işaretleri metin düğümü içinde anlam taşımaz. Öznitelik bağlamında tırnaklar da dönüştürülür, çünkü tırnak öznitelik değerinin sınırını belirler. Bu iki kuralın karışması sık görülen bir kusurdur: gövde kuralıyla kaçırılmış bir değeri tırnaklı bir özniteliğe yazmak, değerin özniteliği erken kapatmasına izin verir.
Adres bağlamında kural tümüyle başkadır. & karakteri HTML varlığına değil, yüzde
kodlamasına çevrilir; çünkü buradaki ayrıştırıcı HTML ayrıştırıcısı değil adres
çözümleyicisidir. Betik bağlamında ise dizgi sınırlayıcısı ve ters bölü karakterleri
dönüştürülür; < karakterinin \u003c biçimine çevrilmesi, dizginin içindeki metnin
kuşatan betik ögesini erken sonlandırmasını engeller.
Bu iç içelik bir kuralı doğurur: bir değer birden çok bağlamdan geçiyorsa kaçırma da iç içe uygulanır. Betik içinde yazılan bir adres için önce adres kodlaması, sonra betik kaçırması gerekir; sıra ters çevrilirse ikinci dönüşüm birincinin ürettiği karakterleri bozar.
Kaçırmanın Yetmediği Yer: Şema Denetimi
Bazı öznitelikler değeri metin olarak değil, adres olarak yorumlar. Bir bağın hedefi kaçırılmış olsa bile bir çalıştırılabilir şemaya işaret ediyorsa, kaçırma hiçbir şey değiştirmez: karakterlerin hiçbiri HTML açısından anlamlı değildir. Ölçüm notundaki kullanıcı bağlarını sayfaya koyan kod, bu yüzden şemayı ayrıca denetler.
// sema.mjs — bag hedefinin semasini izin listesine gore dogrular const IZINLI_SEMA = new Set(['http:', 'https:', 'mailto:']); function bagHedefi(ham, taban) { let adres; try { adres = new URL(ham, taban); } catch { return { izinli: false, neden: 'cozumlenemedi' }; } if (!IZINLI_SEMA.has(adres.protocol)) { return { izinli: false, neden: `sema listede yok: ${adres.protocol}` }; } return { izinli: true, hedef: adres.href }; } const taban = 'https://olcum.ornek.test/notlar/'; const adaylar = [ '/istasyon/kuzey-yamac', 'https://veri.ornek.test/rapor.csv', 'mailto:[email protected]', 'veri-semasi:text/html;base64,SSBhbSB0ZXh0', 'olcum://kuzey', '::bozuk::', ]; for (const aday of adaylar) { const sonuc = bagHedefi(aday, taban); console.log(String(sonuc.izinli).padEnd(6), aday.padEnd(44), sonuc.hedef ?? sonuc.neden); }
$ node sema.mjs true /istasyon/kuzey-yamac https://olcum.ornek.test/istasyon/kuzey-yamac true https://veri.ornek.test/rapor.csv https://veri.ornek.test/rapor.csv true mailto:[email protected] mailto:[email protected] false veri-semasi:text/html;base64,SSBhbSB0ZXh0 sema listede yok: veri-semasi: false olcum://kuzey sema listede yok: olcum: true ::bozuk:: https://olcum.ornek.test/notlar/::bozuk::
Son satır kuralın nasıl yazılması gerektiğini gösterir. ::bozuk:: geçerli bir mutlak
adres değildir, ama bir taban adresle çözümlendiğinde göreli yol sayılır ve sayfanın kendi
şemasını devralır. Denetim ham dizgi üzerinde değil, çözümlenmiş adres üzerinde
yapılır; izin listesi de reddedilecekleri saymak yerine kabul edilecekleri sayar. Reddedilenleri
saymak, listeye girmemiş her şemayı sessizce geçirir.
Kaçırmayı Elle Yapmamak
Yukarıdaki işlevler kuralı göstermek için yazıldı; üretim kodunda her yazma noktasında elle çağrılmaları beklenmez. Bileşen Tabanlı Arayüz Geliştirme kursunda tanıtılan bildirimsel oluşturma modelinin güvenlik açısından en önemli özelliği budur: ifade yerine konan değerler öntanımlı olarak metin düğümü sayılır ve bağlamına uygun biçimde kaçırılır. Kaçırma öntanımlı, ham işaretleme ekleme ise açıkça istenen bir kaçış deliğidir.
Bu kapıların hepsinin ortak yanı, adlarının veya belgelerinin tehlikeyi bildirmesidir: ham
HTML atayan özellik, işaretleme ayrıştırıp ekleyen yöntem, dizgiden bileşen üreten
yardımcılar. Denetim kuralı sadedir: bu kapıların geçtiği yerler sayılabilir olmalı ve her
biri için “buraya gelen veri nereden geliyor” sorusunun yazılı bir yanıtı bulunmalıdır.
Metin yazmak yeterliyse textContent kullanılır; innerHTML bir ihtiyaç değil, bir
karardır.
Kullanıcının biçimlendirilmiş metin girmesi gerçekten gerekiyorsa sorun kaçırma sorunu olmaktan çıkar ve arındırma (sanitization) sorununa dönüşür: işaretlemeyi ayrıştırıp yalnız izin listesindeki ögeleri, öznitelikleri ve şemaları bırakan bir katman gerekir. Arındırma kaçırmadan belirgin biçimde zordur, çünkü ayrıştırma davranışlarındaki farklılıklara duyarlıdır. Kural olarak arındırıcı elle yazılmaz; bakımı sürdürülen bir uygulaması kullanılır ve çıktısı, bu dersin sonraki katmanı olan politika denetimiyle birlikte savunulur.
Özet
- Siteler arası betik çalıştırma, verinin kod olarak yorumlanabildiği bir yere dönüşümsüz yerleştirilmesinden doğar; çalışan kod belgenin kaynağının yetkilerini devralır.
- Üç oluşum yolu vardır: yansıyan, depolanmış ve DOM tabanlı. Sonuncusunda veri sunucuya hiç uğramadığı için savunma yalnız istemci kodunda kurulabilir.
- Savunma girişte süzmek değil, çıkışta bağlama uygun kaçırmaktır; HTML gövdesi, öznitelik, adres ve betik bağlamlarının kuralları birbirinin yerine geçmez.
- Adres yorumlayan öznitelikler için kaçırma yetmez; çözümlenmiş adresin şeması bir izin listesine karşı denetlenir.
- Bildirimsel oluşturma kaçırmayı öntanımlı yapar; ham işaretleme ekleyen kaçış kapıları sayılabilir ve gerekçelendirilebilir olmalıdır.
Sonraki Adım
Kaçırma, uygulamanın kendi yazdığı her yazma noktasında doğru davranmasına bağlıdır. Tek bir noktanın atlanması savunmanın tamamını geçersiz kılar ve büyük bir arayüzde bunun gözden kaçmayacağını varsaymak gerçekçi değildir. Sonraki ders, kaçırmanın altına ikinci bir katman koyar: tarayıcıya hangi kaynaktan gelen betiğin çalıştırılabileceğini bildiren ve bu kararı belgenin kendisinden bağımsız hale getiren içerik güvenlik politikası.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.