Hadi itiraf ederek başlayalım, hepimiz bir noktada o 'acemi' heyecanına yenik düştük. Kariyerimin ilk yıllarındayım, bir e-ticaret platformu için sızma testi yapıyorum. Bir URL parametresinin sayfaya doğrudan yansıdığını fark ettim. 'Tamam' dedim, 'Buradan yürürüm'. O kadar emindim ki kendimden, basit bir script yerine, tarayıcıyı kasmaya çalışan, her türlü DOM elementini manipüle eden devasa bir payload yapıştırdım. Sonuç? Sitede açık buldum bulmasına ama o devasa JS yükü yüzünden müşterinin canlı ortamdaki Single Page Application (SPA) state yönetimini bozdum. Siteye giren herkes bembeyaz bir ekranla karşılaşıyordu. Yaklaşık 45 dakika boyunca 'testCompany' mühendisleriyle beraber o düğümü çözmeye çalıştık. O gün öğrendiğim en büyük ders şuydu: Tarayıcı, sandığımızdan çok daha tehlikeli bir oyun alanı.
Sunucu Artık Güvenli (Mi acaba?)
Yıllardır 'input validation' diye bağırdık, durduk. Artık çoğu framework (React, Vue, Angular) bizi sunucu taraflı saldırılardan bir nebze koruyor. Ama asıl tehlike sinsice yer değiştirdi. Eskiden her şey PHP veya Java tarafında biterken, şimdi mantık (logic) istemciye, yani kullanıcının tarayıcısına kaydı. İşte tam burada DOM-based Cross-Site Scripting (XSS) devreye giriyor.
Normal bir XSS saldırısında payload sunucuya gider ve geri döner. Ama DOM-based olanda sunucunun ruhu bile duymaz. Her şey window.location.hash gibi istemci tarafındaki objelerde döner. Sunucu loglarına baktığında her şey tertemiz görünür, ama kullanıcının dünyası çoktan tepetaklak olmuştur.
Kaynaklar ve Hedefler (Sources and Sinks)
DOM XSS mantığını anlamak için iki kavramı çok iyi bilmek lazım: Source (Kaynak) ve Sink (Hedef/Alıcı).
Source, saldırganın kontrol edebildiği veridir. Örneğin: location.search, location.hash, document.referrer hatta localStorage.
Sink ise bu verinin işlendiği ve tarayıcı tarafından 'çalıştırıldığı' tehlikeli fonksiyondur. Klasik suçlular: eval(), setTimeout(), setInterval() ve tabii ki en sevdiğimiz element.innerHTML.
Şöyle bir senaryo düşünün, testCompany sitesinde kullanıcıya ismiyle hitap eden bir karşılama ekranı var:
// Hatalı ve tehlikeli kullanım
const urlParams = new URLSearchParams(window.location.search);
const userName = urlParams.get('name');
const welcomeMessage = document.getElementById('welcome-msg');
// İşte felakete davetiye çıkaran satır
welcomeMessage.innerHTML = "Hoş geldin, " + userName;
Burada saldırgan şöyle bir URL ile gelirse ne olur?
https://example.com/dashboard?name=<img src=x onerror=alert(document.domain)> (Zararsızlaştırılmış/Defanged)
Tarayıcı bu URL'i aldığında sunucuya ?name=... kısmını gönderir, ancak modern tarayıcılar ve JS motorları bunu işlerken innerHTML üzerinden o <img> etiketini DOM ağacına ekler. src=x hatalı olduğu için onerror tetiklenir ve bingo! Kodumuz kullanıcının tarayıcısında, o sitenin yetkileriyle çalışmaya başlar.
Neden WAF'lar Bizi Kurtarmıyor?
Birçok güvenlik ekibi 'Önümüze bir WAF (Web Application Firewall) koyarız, tüm XSS'leri engeller' diye düşünür. Üzgünüm ama yanılıyorsunuz. DOM-based XSS'in en büyük numarası, payload'un bazen sunucuya hiç uğramamasıdır.
Özellikle # (fragment) karakterinden sonra gelen veriler sunucuya HTTP isteğiyle gitmez.
https://example.com/#page?user=<script>alert(1)</script>
Burada sunucu sadece https://example.com/ kısmını görür. Geri kalan her şey tarayıcı içindeki JavaScript tarafından parse edilir. WAF'ın görmediği bir şeyi engellemesini bekleyemezsiniz.
Modern Defans: Güvenli Limanlara Yelken Açmak
'Peki ne yapacağız Sedat? Hiç mi dinamik içerik basmayacağız?' dediğinizi duyar gibiyim. Tabii ki basacağız, ama akıllıca. İşte sahadan birkaç sağlam defans stratejisi:
1. innerHTML Yerine textContent
En basit ve en etkili yöntem budur. Eğer HTML taglerine ihtiyacınız yoksa, sadece metin basacaksanız textContent kullanın. Tarayıcı bunu 'çalıştırılabilir kod' olarak değil, saf metin olarak görür.
// Güvenli kullanım
welcomeMessage.textContent = "Hoş geldin, " + userName;
2. Trusted Types API
Modern tarayıcılarda (özellikle Chrome tarafında) hayatımıza giren muazzam bir özellik var: Trusted Types. Bu, tehlikeli sink'lere (innerHTML gibi) doğrudan string atamanızı engeller. Önce o string'in 'güvenli' olduğunu bir policy üzerinden geçirip kanıtlamanız gerekir.
// Trusted Types Policy tanımlama
const escapePolicy = trustedTypes.createPolicy('myEscapePolicy', {
createHTML: (string) => string.replace(/</g, '<').replace(/>/g, '>')
});
// Artık sadece bu policy'den geçmiş veriyi kabul eder
welcomeMessage.innerHTML = escapePolicy.createHTML(userName);
3. Content Security Policy (CSP)
CSP sizin en dıştaki koruma kalkanınızdır. Doğru yapılandırılmış bir CSP, inline scriptlerin çalışmasını engelleyebilir veya sadece belirli domainlerden script yüklenmesine izin verebilir.
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-scripts.example.com;
Bu kural setinde, saldırgan araya bir <script> sokuşturmaya çalışsa bile tarayıcı 'Hop kardeşim, bu script benim güvenli listemde yok' diyerek çalıştırmayı reddeder.
Sahadan Bir Tavsiye: 'Sink' Avcılığı
Red Team operasyonlarımızda biz genellikle uygulamanın kaynak kodlarını indirip basit bir grep veya IDE aramasıyla işe başlarız. Siz de kendi projelerinizde şunu yapın: Kodunuzu açın ve innerHTML, outerHTML, document.write, eval() kelimelerini aratın.
Bulduğunuz her sonuç potansiyel bir sızıntı noktasıdır. Bu noktaların 'Source' (yani URL parametresi, cookie, localstorage) ile beslenip beslenmediğini takip edin. Eğer besleniyorsa, orada bir problem var demektir.
Kapanışa Doğru
Web güvenliği statik bir şey değil, sürekli devinen bir yapı. Bugün 'güvenli' dediğimiz framework, yarın yeni bir bypass yöntemiyle sarsılabilir. Önemli olan, verinin tarayıcıya girdiği andan, ekrana basıldığı ana kadar olan yolculuğunu takip edebilmek.
İlk sızma testimde o veritabanını çökertmeseydim (veya state'i bozmasaydım), belki de bu kadar derinlemesine incelemeyecektim bu konuları. Hatalar iyidir, yeter ki aynı hatayı iki kere yapmayalım.
Bir sonraki yazıda, bu DOM sızıntılarının daha derinlerine, yani Prototype Pollution dünyasına dalarız belki. O zamana kadar; sink'lerinizi temiz, source'larınızı kontrollü tutun. Güvenli kodlamalar!
