İçeriğe geç
academia.sh

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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;' };
const OZNITELIK = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };

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 &lt;isaret&gt; &amp; "kuzey" yamaci
Oznitelik degeri   12&#39;lik &lt;isaret&gt; &amp; &quot;kuzey&quot; 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat