Loading...

Güvenlik · 11 dk okuma

XSS nedir? Cross-site scripting ve kapıyı kapatmanın yolu

Cross-site scripting (XSS), saldırganın kendi JavaScript'ini sizin sayfanızda, ziyaretçilerinizin tarayıcısında, sitenizin yetkileriyle çalıştırmasına izin veren bir hatadır. Güvenilmeyen metin sayfaya düz metin olarak değil, markup ya da kod olarak ulaştığında olur. Çözüm kodunuzdadır: çıktıyı bağlamına göre kodlayın, kabul etmek zorunda olduğunuz HTML'i sanitize edin, ikinci hat olarak HttpOnly çerezler ve bir Content-Security-Policy ekleyin.

Güncellendi

XSS nedir? Cross-site scripting ve kapıyı kapatmanın yolu

Cross-site scripting nedir

Cross-site scripting (XSS), saldırganın sitenizdeki bir sayfaya kendi yazdığı JavaScript'i dahil ettirdiği bir zafiyettir. Tarayıcı o script'i sizin script'lerinizden ayırt edemez: sizin origin'inizden gelir, bu yüzden kullanıcılarınızın oturumları dahil origin'inizin yapmaya izinli olduğu her şeyle çalışır.

Kök neden hep aynıdır. Başka birinin kontrol ettiği metin, örneğin bir yorum, bir arama terimi, bir görünen ad ya da bir URL fragment'ı, sayfaya tarayıcının onu düz metin yerine markup ya da kod olarak okumasına izin verecek şekilde eklenir. Ekranda <script> karakterleri olarak görünmesi gereken bir yorum, bunun yerine bir script elemanına dönüşür.

Ad tarihseldir ve biraz yanıltıcıdır: klasik saldırıda ikinci bir site vardı, ama bugün XSS'in çoğu sadece kendi sayfanızın içinde çalıştırılan güvenilmeyen veridir. OWASP onu OWASP Top 10'un Injection kategorisine, sunucu tarafındaki kuzeni SQL injection'ın yanına koyar. Fark yorumlayıcıdadır. SQL injection veritabanınızı kandırır; XSS kullanıcılarınızın tarayıcılarını.

XSS uygulamadaki bir hatadır; tarayıcıda ya da ağda değil. HTTPS onu engellemez, bir güvenlik duvarı tamamen engellemez ve onu yalnızca sayfayı oluşturan kod ortadan kaldırabilir.

Üç tür: stored, reflected ve DOM tabanlı

Stored XSS (kalıcı, persistent da denir) en zarar verici olanıdır. Kötü niyetli girdi sunucu tarafından bir yorumda, bir profil alanında, bir ürün değerlendirmesinde ya da bir destek talebinde saklanır ve sonra o sayfayı açan her ziyaretçiye sunulur; çoğu zaman yönetim panelini okuyan yöneticiler de buna dahildir. Kimsenin özel bir linke tıklaması gerekmez.

Reflected XSS saklanmaz. Sunucu istekten bir şey alır, tipik olarak bir query parametresi, ve onu doğrudan yanıta geri yazar: "... için sonuçlar" yazan bir arama sayfası ya da hatalı değeri tekrarlayan bir hata sayfası. Saldırganın kurbana e-posta, sohbet ya da başka bir site üzerinden hazırlanmış bir linki açtırması gerekir.

DOM tabanlı XSS tamamen tarayıcıda olur. Kendi JavaScript'iniz saldırganın kontrol ettiği bir değeri, örneğin location.hash, location.search, postMessage verisi ya da document.referrer'ı okur ve tehlikeli bir sink'e yazar: innerHTML, document.write, eval, string ile setTimeout ya da bir href içindeki javascript: URL'i. #'den sonraki her şey istekle gönderilmediği için sunucu payload'ı hiç görmeyebilir.

Aşağıdaki üç ders kitabı örneği her hatanın biçimini gösteriyor. Her durumda çözüm aynı fikirdir: değeri metin olarak ele almak.

Açıklı en küçük kalıplar (bunları yayına almayın)

<!-- Stored: a saved comment printed without escaping (PHP) -->
<p><?= $comment->body ?></p>
<!-- body saved as: <script>alert(document.domain)</script> -->

<!-- Reflected: the search term echoed from the URL -->
<h1>Results for <?= $_GET['q'] ?></h1>
<!-- link: /search?q=<script>alert(document.domain)</script> -->

// DOM-based: client code writes the URL fragment as HTML
document.getElementById('greeting').innerHTML =
  decodeURIComponent(location.hash.slice(1));
// link: /welcome#<img src=x onerror=alert(document.domain)>

Saldırgan bununla ne yapabilir

alert(1) test edenlerin hatayı kanıtlama yoludur; saldırganların çalıştırdığı şey bu değildir. Script'leri sayfanızda çalıştığı anda, origin'inizin içinde giriş yapmış kullanıcı gibi davranır.

Oturum hırsızlığı. Oturum çerezi JavaScript'ten okunabiliyorsa script onu saldırgana gönderir, saldırgan da kurban olarak giriş yapar. localStorage ya da sessionStorage'da tutulan token'lar script tarafından her zaman okunabilir; uzun ömürlü token'ları orada saklamaya karşı ana argüman budur.

Kullanıcı adına işlemler. Çerez HttpOnly olsa bile script fetch ile API'nizi çağırabilir; çerezi tarayıcı kendisi ekler. Sayfadan CSRF token'larını okuyabilir, hesaptaki e-posta adresini değiştirebilir, bir yönetici kullanıcı oluşturabilir, mesaj gönderebilir ya da sipariş verebilir. Same-origin kuralları, CORS ve SameSite çerezler bunu durdurmaz, çünkü istek sizin kendi sitenizden gelir.

Kullanıcının gördüğünü okumak. Kişisel veriler, mesajlar, faturalar; sayfadaki ya da API'niz üzerinden ulaşılabilen her şey.

Alan adınızda phishing ve keylogging. Script sayfayı bir giriş formu olarak yeniden çizebilir ya da kullanıcının gerçek formlara yazdıklarını kaydedebilir. Adres çubuğu alan adınızı ve geçerli bir sertifikayı gösterir; kullanıcının şüphelenmesi için bir neden yoktur.

Tahrifat ve yayılma. Stored XSS her ziyaretçinin gördüğünü değiştirebilir ya da kendini her kurbanın kendi profiline kopyalayabilir. MySpace'teki 2005 Samy solucanı bu yolla bir günden kısa sürede bir milyondan fazla profile yayıldı.

Belirli bir XSS'in ne kadar kötü olduğu, sayfayı kimin gördüğüne bağlıdır. Yalnızca yöneticilerin gördüğü ve bir destek talebi konusu gibi saklanan bir girdiyle ulaşılan bir ekrandaki hata, çoğu zaman herkese açık ana sayfadakinden daha kötüdür.

Çözüm: bağlama duyarlı çıktı kodlaması

Temel savunma, güvenilmeyen veriyi sayfaya yazdığınız anda, çevresindeki bağlamın gerektirdiği şekilde kodlamaktır. Girdiyi doğrulamak yardımcı olur (bir posta kodu posta koduna benzemelidir), ama ana kontrol olamaz, çünkü aynı değer bir bağlamda güvenli, başka bir bağlamda tehlikeli olabilir.

HTML gövdesi: &, <, >, " ve ' karakterlerini entity'lere çevirin; böylece <script> ayrıştırılmak yerine görüntülenir.

HTML özniteliği: öznitelik değerlerini her zaman tırnak içine alın ve aynı karakterleri escape edin. Tırnaksız bir öznitelikten tek bir boşlukla çıkılabilir.

href ya da src içindeki URL: kodlama yetmez, çünkü javascript:alert(1) escape edilecek hiçbir şey içermez. URL'i ayrıştırın ve yalnızca https: ve http:'ye (gerekiyorsa mailto:'ya da) izin verin; query değerlerini encodeURIComponent ile kodlayın.

Bir <script> bloğunun içinde: JavaScript'e string birleştirmeyin. Veriyi < karakterini de escape eden bir fonksiyonla JSON olarak serileştirin ya da bir data- özniteliğine koyup element.dataset ile okuyun.

Stiller: kullanıcı verisini CSS'e hiç koymayın.

İstemcide: güvenli API'leri tercih edin. textContent, zararsız özniteliklerde setAttribute ve createElement asla HTML ayrıştırmaz; innerHTML, outerHTML, insertAdjacentHTML ve document.write ayrıştırır.

Kullanıcı verisini tarayıcıda güvenle yazmak

// Text, never markup
el.textContent = userName;

// Links: allow only http(s)
function safeUrl(value) {
  try {
    const url = new URL(value, location.origin);
    return ['https:', 'http:'].includes(url.protocol) ? url.href : '#';
  } catch {
    return '#';
  }
}
link.href = safeUrl(profile.website);

// Data for scripts: a data attribute, not string concatenation
// <div id="app" data-user="{{ user_json }}">  (escaped by the template)
const user = JSON.parse(document.getElementById('app').dataset.user);

Framework'lerin otomatik escape'i ve kaçış kapıları

Modern framework'ler HTML bağlamı için kodlamayı zaten varsayılan olarak yapar. Blade, Twig, Jinja, Django şablonları, Vue ve Angular'daki {{ }}, Rails ERB'deki <%= %> ve React JSX'teki {value} yazdırdıklarını escape eder. XSS'in elle yazılmış PHP sayfalarına göre daha nadir olmasının ana nedeni ve değerleri framework'ün varsayılan yoluyla yazdırmaya devam etmenin nedeni budur.

Modern bir kod tabanındaki gerçek XSS'lerin neredeyse tamamı bir kaçış kapısında, escape'i bilerek kapatan bir özellikte yaşar:

- React dangerouslySetInnerHTML, Vue v-html, Svelte {@html}, Angular bypassSecurityTrustHtml. - Laravel Blade {!! $value !!}, Jinja |safe ve Markup(), Django mark_safe ve {% autoescape off %}, Rails raw ve html_safe. - Bileşen kodundan doğrudan DOM yazımları: ref.current.innerHTML = ..., jQuery .html(), $(userInput).

Diğer boşluklar, otomatik escape'in anlamadığı bağlamlardır. HTML'i escape eden bir şablon href={url} içinde javascript:'i yine geçirir, satır içi bir <script> ya da event handler içindeki kullanıcı değerinin kodu bozmasına yine izin verir ve eval'e verdiğiniz bir string'i asla korumaz.

Pratik bir kural: kaçış kapılarını nadir, aranabilir ve incelenmiş yapın. Yukarıdaki liste için yapılan bir kod araması açığınızın çoğunu bulur; bir linter kuralı da (örneğin ESLint react/no-danger ya da {!! için bir Semgrep kuralı) yenilerinin fark edilmeden girmesini önler.

HTML kabul etmek zorundaysanız: gerçek bir sanitizer kullanın

Bazı özellikler kullanıcı HTML'i gerektirir: zengin metin editörü, bir CMS gövdesi, biçimlendirmeli bir forum gönderisi, bir e-posta önizlemesi. Escape biçimlendirmeyi yok eder; bu yüzden cevap sanitize etmektir: HTML'i ayrıştırıp yalnızca güvenli eleman ve özniteliklerden oluşan bir izin listesini tutmak.

Bu iş için yapılmış, bakımı süren bir kütüphane kullanın; asla bir düzenli ifade ya da "kötü" etiketlerden oluşan bir engel listesi değil. Tarayıcılar HTML'i şaşırtıcı biçimlerde ayrıştırır ve <script>'i silen elle yazılmış her filtre bir event handler özniteliğiyle, bir SVG elemanıyla, tuhaf iç içe geçmeyle ya da bir kodlama hilesiyle atlatılmıştır. Yerleşik seçenekler: tarayıcıda ve jsdom ile Node.js'te DOMPurify, PHP için HTML Purifier, Python için nh3 (Rust kütüphanesi ammonia) ve Java için OWASP Java HTML Sanitizer.

Üç ayrıntı farkı yaratır. Özelliğin ihtiyaç duyduğu kadar küçük bir izin listesiyle sanitize edin. Çıktıda sanitize edin ya da çıktıda yeniden sanitize edin; böylece kütüphanedeki ya da izin listenizdeki sonraki bir değişiklik eski veriyi de korur. Ve HTML'i sanitize ettikten sonra değiştirmeyin: yeniden ayrıştırmak, string değiştirmek ya da onu farklı bir bağlama eklemek güvenli bir sonucu güvensiz hale getirebilir.

Markdown'da da işlenmiş HTML aynı muameleyi gerektirir: çoğu Markdown işleyici varsayılan olarak ham HTML'e izin verir.

Hasarı sınırlayın: HttpOnly, SameSite ve Content-Security-Policy

Bir XSS'in eninde sonunda gözden kaçacağını varsayın ve onu daha az değerli kılın.

Çerezler. Oturum çerezlerini document.cookie okuyamasın diye HttpOnly, ayrıca Secure ve SameSite=Lax ya da Strict olarak işaretleyin. Bu, çalınan çerez senaryosunu durdurur. Sayfa açıkken script'in kullanıcı gibi davranmasını durdurmaz; bu yüzden bir çözüm değil, hasar kontrolüdür. Aynı nedenle uzun ömürlü token'ları localStorage'dan uzak tutun.

Content-Security-Policy. CSP tarayıcıya hangi script'lerin çalışabileceğini söyler. XSS'i durduran politika katı (strict) olandır: her yanıt için yeniden üretilen, hem başlığa hem de her meşru <script> etiketine konan rastgele bir nonce, artı 'strict-dynamic', object-src 'none' ve base-uri 'none'. Enjekte edilmiş bir <script>'in geçerli bir nonce'u yoktur ve onerror= gibi satır içi event handler'lar engellenir; böylece çoğu XSS hatası bir konsol hatasına ve bir rapora dönüşür. Yalnızca alan adlarını listeleyen ve 'unsafe-inline''ı tutan bir politika XSS'e karşı az koruma sağlar.

Önce Content-Security-Policy-Report-Only olarak yayına alın, raporların gösterdiklerini düzeltin, sonra uygulamaya geçin. HTTP güvenlik başlıkları rehberi, report-only geçişini, yanında olması gereken diğer başlıkları ve X-XSS-Protection'ın neden artık gönderilmemesi gerektiğini anlatıyor.

Bir önbellek tuzağı: nonce tahmin edilemez olmalıdır. Bir CDN ya da sayfa önbelleği aynı HTML'i herkese sunarsa her ziyaretçi aynı nonce'u alır ve saldırgan onu sayfadan okuyabilir. Önbelleğe alınan sayfalarda bunun yerine script hash'leri ('sha256-...') kullanın ya da nonce taşıyan HTML'i önbelleğin dışında tutun.

Tarayıcıların desteklediği yerlerde require-trusted-types-for 'script' daha da ileri gider: innerHTML gibi DOM sink'leri düz string'leri reddeder; böylece DOM tabanlı XSS sizin yazdığınız koddan geçmek zorunda kalır.

Katı, nonce tabanlı bir politika (her yanıtta yeni nonce)

Content-Security-Policy: script-src 'nonce-R4nd0mPerResponse' 'strict-dynamic'; object-src 'none'; base-uri 'none'

<script nonce="R4nd0mPerResponse" src="/js/app.js"></script>

Set-Cookie: session=...; Path=/; Secure; HttpOnly; SameSite=Lax

Kendi uygulamanızı XSS için test etmek

Yalnızca sahibi olduğunuz ya da test etmeye yetkili olduğunuz sistemleri test edin. Kendi uygulamanız için zararsız bir işaretçi, hiçbir saldırı kodu olmadan hataların çoğunu bulur.

Her girdiyi her çıktıya kadar izleyin. HTML için anlamlı karakterler içeren benzersiz bir işaretçiyi, örneğin xss7"'<b>bold</b>, her alana, her query parametresine, uygulamanızın gösterdiği her başlığa ve kabul ettiğiniz her dosya adına koyun. Sonra yönetim ekranları, e-postalar, dışa aktarmalar ve bildirimler dahil o değerin göründüğü her sayfayı ziyaret edin. Kelime kalın görünüyorsa ya da sayfanın kaynağı <b>'yi escape edilmemiş veya bir özniteliği kapatan bir tırnak gösteriyorsa, çıktı kodlanmıyordur.

Yalnızca kaynağı değil, DOM'u kontrol edin. DOM tabanlı hatalar için işaretçiyi location.hash'e ve query string'e koyun ve geliştirici araçlarında canlı DOM'u inceleyin: view-source yalnızca sunucunun gönderdiğini gösterir.

Kodda arayın. Yukarıda listelenen kaçış kapılarını ve innerHTML, insertAdjacentHTML, document.write, eval ve new Function'ı grep'leyin. Her eşleşme ya kaldırılmalı ya da girdinin neden güvenli olduğunu açıklayan kısa bir yorumu olmalıdır.

Araç kullanın. OWASP ZAP ve Burp Suite girdileri otomatik olarak tarar ve test eder, bariz reflected durumları bulur; Semgrep ya da CodeQL gibi statik analiz araçları bir tarayıcının göremeyeceği veri akışlarını izler. İkisi de yönetim ekranlarındaki stored XSS için elle yapılan izlemenin yerini tutmaz.

CSP raporlarını izleyin. Report-only bir politika devreye girdiğinde, beklenmedik satır içi script'lerden gelen ihlaller bedava bir erken uyarıdır.

Bir kod tabanındaki kaçış kapılarını bulun
grep -rnE 'dangerouslySetInnerHTML|v-html|\{@html|bypassSecurityTrust|innerHTML|insertAdjacentHTML|document\.write' src/
grep -rnE '\{!!|\|safe|mark_safe|html_safe|raw\(' resources/ templates/ app/

WAF'ın yeri ve CDN.com.tr WAF'ının yaptığı

Bir web uygulama güvenlik duvarı (WAF), istekleri inceler ve saldırıya benzeyenleri engeller. XSS için bu faydalıdır: bir query string ya da form gövdesindeki script etiketleri ve event handler'lar tanınabilir; bu yüzden reflected denemeler ve otomatik tarayıcılar uygulamanıza ulaşmadan durdurulur, normal bir formdan gönderilen stored payload da çoğu zaman girişte reddedilir.

Bir katmandır, çözüm değildir. WAF sayfalarınızı değil istekleri görür; bu yüzden bir değerin sonra nasıl kullanılacağını bilemez. URL fragment'ında yaşayan DOM tabanlı XSS ona hiç ulaşmaz; bir içe aktarma, çağırdığınız bir API ya da WAF'ın incelemediği bir kanal üzerinden gelen girdi arasından sızar; saldırganlar da payload'ları kalıplardan kaçacak şekilde ayarlar. OWASP Top 10 rehberi, bir güvenlik duvarının neyi yakalayıp neyi yakalayamadığını ayrıntılı olarak çıkarıyor. Önde WAF olsa da olmasa da çıktıyı kodlayın, HTML'i sanitize edin ve bir CSP ayarlayın.

CDN.com.tr'de WAF, edge'de çalışan ve hesap için Yayınlama Kuralları sayfasından açılan OWASP Core Rule Set ile ModSecurity'dir. Core Rule Set'in 941 ailesi cross-site scripting kurallarıdır. Engellenen ziyaretçi referans ID içeren markalı bir 403 sayfası alır; WAF Logs sayfası engellenen olayları saldırı kategorisi, ülke, IP ve kuralla listeler, son 30 gün içinde o ID ile aranabilir ve CSV ya da XLSX olarak dışa aktarılabilir; cdnctl waf logs aynı listeyi komut satırından döndürür.

XSS kurallarının olağan false positive'i meşru bir HTML gönderisidir; örneğin biçimlendirilmiş içerik ya da bir kod örneği kaydeden bir editör. Tek yollar için istisnalar henüz panelde yok: WAF meşru bir isteği engellerse, WAF'ı kapatmak yerine referans ID'sini desteğe gönderin.

Başlıklar edge'de de eklenebilir. Tek değerli başlıklar bir dağıtım kuralının Özel başlıklar alanına, her satıra bir tane gelecek şekilde yazılır ve önbellek isabetleri dahil 2xx ve 3xx yanıtlarına uygulanır. Tam bir Content-Security-Policy oraya konamaz, çünkü panel ve edge başlık satırında ; kabul etmez; frame-ancestors 'self' gibi tek bir direktif çalışır, nonce ise zaten her yanıt için üretilmek zorundadır; bu yüzden politikanın yeri uygulamanızdır.

Engellenen XSS denemeleri komut satırından
cdnctl waf logs --account $ACCOUNT_UUID --range 1d
cdnctl waf show $REFERENCE_ID --account $ACCOUNT_UUID

XSS hakkında sık sorulanlar

Stored, reflected ve DOM tabanlı XSS arasındaki fark nedir?

Payload'ın nerede yaşadığı. Stored XSS sunucuda saklanır ve sayfayı açan herkese sunulur. Reflected XSS aynı istekten geri gelir; bu yüzden kurbanın hazırlanmış bir linki açması gerekir. DOM tabanlı XSS'i ise kendi istemci tarafı JavaScript'iniz, saldırganın kontrol ettiği bir değeri, çoğu zaman URL'den, sayfaya yazarak oluşturur; sunucu onu hiç görmeyebilir.

HTTPS ya da geçerli bir sertifika XSS'e karşı korur mu?

Hayır. TLS aktarım halindeki veriyi korur. XSS payload'ı kendi sunucunuz ya da kendi script'iniz tarafından aynı şifreli bağlantı üzerinden teslim edilir ve asma kilit, alan adınızdaki sahte bir giriş formunu daha az değil, daha güvenilir gösterir.

HttpOnly XSS'i durdurmaya yeter mi?

Hayır. Script'in oturum çerezini okumasını durdurur; bu da bir sonucu ortadan kaldırır. Script yine kullanıcı adına istek gönderebilir, çünkü çerezi onun için tarayıcı ekler; sayfayı da yine okuyup değiştirebilir. HttpOnly hasar kontrolüdür; çözüm çıktı kodlamasıdır.

WAF cross-site scripting'i durdurabilir mi?

İsteklerdeki script payload'ları tanınabildiği için reflected ve otomatik denemelerin çoğunu durdurur. URL fragment'ındaki DOM tabanlı XSS'i göremez, uygulamanızın saklanan bir değeri nasıl kullanacağını bilmez ve özel olarak uyarlanmış bir payload ile atlatılabilir. Onu doğru kodun önünde bir katman olarak görün.

X-XSS-Protection göndermeye devam etmeli miyim?

Hayır. Kontrol ettiği filtre güncel tarayıcılardan kaldırıldı, eski tarayıcılarda ise kötüye kullanılabiliyordu. Hiçbir şey göndermeyin ya da X-XSS-Protection: 0 gönderin ve bunun yerine bir Content-Security-Policy kullanın.

React ya da Vue uygulamamı XSS'e karşı bağışık yapar mı?

Yazdırdığınızı escape ederek yaygın durumu güvenli hale getirirler. Yine de dangerouslySetInnerHTML ve v-html üzerinden, href içinde javascript: ile başlayan kullanıcı URL'leri üzerinden, doğrudan DOM yazımları üzerinden ve framework'ün kontrol etmediği sunucu tarafında üretilmiş HTML üzerinden açıksınız.

Self-XSS nedir?

Kurbanın kandırılarak kendi tarayıcı konsoluna yapıştırdığı bir script. Sitenizde hiçbir hata gerektirmez; tarayıcıların geliştirici araçlarına yapıştırma yaptığınızda uyarmasının nedeni budur. Kodunuzdaki bir zafiyet değil, sosyal mühendisliktir.