İçeriğe atla
0 XP 0
13 dkderinleşmeyle 15 dk50 XP7 etkinlik

Bağlama göre çıktı kodlama

Bu derste
  • XSS türlerini, bağlama göre çıktı kodlamayı, şablon motoru tuzaklarını ve CSP ile Trusted Types'tan oluşan ikinci savunma katmanını öğren.
  • Çıktı kodlama, bastığın yerin bağlamına göre değişir; tek bir kaçış fonksiyonu her yere yetmez.
  • Bu derste iade açıklamasının HTML sayfasında güvenle nasıl gösterileceğini kuracaksın.

Puan bütün etkinlikleri cevaplayınca hesaba yazılır. Bu dersi tamamlayınca +50 XP alacaksın. Giriş yapmadan ilerlersen puan yalnız bu tarayıcıda durur.

Doğrulanmış veri güvenli veri demek değildir. AltayKargo iade servisinde açıklama alanı beş yüz karaktere kadar serbest metin kabul eder ve bu doğru bir karardır, çünkü müşteri hasarı kendi cümleleriyle anlatır. Ama aynı metin durum sayfasında görüntülenirken sorun çıkarır.

Müşteri açıklamaya bir betik parçası yazarsa ve sayfa bu metni doğrudan HTML'e basarsa, tarayıcı onu kod sanıp çalıştırır. Bu saldırıya siteler arası betik çalıştırma denir, kısaca XSS. Kök neden veri ile kod arasındaki sınırın karışmasıdır.

Çözüm girdiyi kısıtlamak değil, çıktıyı bastığın yere göre kodlamaktır. XSS, CWE-79 kimliğiyle 2025 CWE Top 25 listesinin birinci sırasındadır. OWASP Top 10:2025'te A05 Enjeksiyon kategorisinin içinde yer alır ve bu kategoride 30 binden fazla CVE kaydıyla en kalabalık zayıflıktır.

Üç tür XSS: yansıyan, depolanan, DOM tabanlı

XSS'i verinin yolculuğuna göre üç türe ayırırsın. Yansıyan XSS'te (reflected) zararlı değer isteğin kendisindedir. Arama kutusuna yazılan metin aynı yanıtta sayfaya geri basılır.

Saldırgan bu değeri taşıyan bir bağlantı hazırlar ve kurbanın tıklamasını bekler. Depolanan XSS'te (stored) değer önce veritabanına yazılır, sonra sayfayı açan herkese gösterilir. AltayKargo'daki iade açıklaması bu türe uyar.

Müşterinin yazdığı metin, iadeyi inceleyen destek personelinin ekranında açılır. Bu yüzden depolanan XSS daha tehlikelidir. Tek bir kayıt, yetkili bir çalışanın oturumunda çalışabilir ve kimsenin bir bağlantıya tıklaması gerekmez.

Üçüncü tür DOM tabanlı XSS'tir. Bu türde sunucu değeri yanıta basmaz, yanıt temiz gelir. Tarayıcıdaki betik, adres çubuğu gibi bir kaynaktan değeri okur ve tehlikeli bir hedefe yazar.

DOM tabanlı XSS'i anlamak için iki kavram yeter: kaynak (source) ve hedef (sink). Kaynak, saldırganın değerini denetleyebildiği yerdir. location.hash, location.search, document.referrer ya da postMessage ile gelen veri birer kaynaktır.

Hedef, gelen metni kod ya da HTML olarak yorumlayan yerdir: innerHTML, outerHTML, document.write ve eval gibi. Kaynaktan hedefe kodlanmadan akan her değer bir açıktır. Aşağıdaki kodda iade durum sayfası, adresteki # işaretinden sonraki metni başlık olarak gösterir.

Adres paylaşılabildiği için saldırgan bu metni istediği gibi kurar. Düzeltme hedefi değiştirmektir. textContent değeri her zaman düz metin olarak yazar ve tarayıcı içindeki etiketi yorumlamaz.

// Önce: kaynak location.hash, hedef innerHTML
const baslik = decodeURIComponent(location.hash.slice(1));
document.getElementById("baslik").innerHTML = baslik;

// Sonra: aynı kaynak, güvenli hedef
document.getElementById("baslik").textContent = baslik;
DOM tabanlı XSS: sorun kaynakta değil, değerin yazıldığı hedefte.
Derinleş:Yirmi saatte bir milyon profil

Depolanan XSS'in neden en tehlikeli tür sayıldığını 2005'teki bir olay iyi gösterir. Bir sosyal ağ kullanıcısı, profil sayfasına sitenin filtresini atlatan bir betik yerleştirdi. Profili ziyaret eden herkesin tarayıcısı betiği çalıştırdı. Betik ziyaretçinin oturumuyla iki iş yaptı: yazara arkadaşlık isteği gönderdi ve kendini ziyaretçinin profiline kopyaladı.

Her yeni profil yeni bir bulaşma noktası oldu. Yayılma 4 Ekim 2005'te başladı ve yirmi saat içinde bir milyonu aşkın kullanıcı betiği çalıştırdı. Site yayılmayı durdurmak için bir süre kapatıldı. Olay, XSS'in yalnız tek bir kurbanı etkileyen bir sorun olmadığını gösterdi.

Depolanan bir betik, kendini kopyalayabildiğinde bir solucana dönüşür. Betik yalnız kullanıcının zaten yapabildiği işleri yaptı, ama bunu kullanıcının haberi olmadan ve onun adına yaptı. Yazar sonradan hukuki süreçle karşılaştı ve denetimli serbestlik cezası aldı. Olay bugün hâlâ güvenlik eğitimlerinde depolanan XSS'in klasik örneği olarak anlatılır.

Etkinlik 1 / 7

Her durumu XSS türüne göre ayır.

Önce bir seçeneğe, sonra aşağıdaki uygun kutuya bas. İstersen seçeneği kutuya sürükleyip bırak. Yerini değiştirmek için aynı işlemi tekrarla.

0/5 seçenek yerleşti.

HTML gövdesinde kodlama ve render anı

Savunmanın ilk halkası HTML gövdesidir, çünkü kullanıcı metninin çoğu oraya basılır. AltayKargo'nun durum sayfası eski bir sunucu şablonuyla üretilir ve iade açıklamasını bir div etiketinin içine yazar. Aşağıdaki ilk kod metni olduğu gibi basar. Açıklamada bir script etiketi varsa tarayıcı onu sayfanın kendi kodundan ayırt edemez ve çalıştırır.

İkinci kod aynı değeri basmadan hemen önce kodlar. Değişiklik tek bir fonksiyon çağrısıdır, ama veri ile kod arasındaki sınırı yeniden çizer.

// Önce: açıklama HTML'e doğrudan basılıyor
res.send(`<div class='aciklama'>${iade.aciklama}</div>`);

// Sonra: metin HTML gövdesine basılmadan önce kodlanıyor
const kacir = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
res.send(`<div class='aciklama'>${kacir(iade.aciklama)}</div>`);
Durum sayfası (sunucu tarafı şablon): açıklama önce ham basılıyor, sonra kodlanarak.

kacir fonksiyonu üç özel karakteri HTML varlığına çevirir; tarayıcı bunları artık etiket değil, düz metin olarak okur. İlk replace çağrısının & için olması tesadüf değildir. Sıra ters olsaydı &lt; içindeki & yeniden kodlanır, ekranda bozuk metin çıkardı. Bu üç karakter yalnız metin içeriği için yeterlidir.

Aynı değer bir öznitelik içine ya da bir JavaScript dizesine basılacaksa ayrı bir kodlayıcı gerekir. React ve Vue gibi kütüphaneler bu işi arka planda otomatik yapar; elle yazmak genelde yalnız eski sunucu şablonlarında gerekir. Tam bir kütüphane ayrıca tırnak işaretlerini de kaçışlar, çünkü öznitelik bağlamında tırnak kırılması ayrı bir açık türüdür.

Kodlama, render anına en yakın yerde uygulanır, kayıt anında değil. Aynı açıklama metni bir HTML sayfasında, bir JSON yanıtında ve bir e-postada ayrı ayrı gösterilebilir; her biri kendi kodlayıcısını ister. Bu yüzden ham veri veritabanında saklanır, kodlama yalnız gösterileceği anda ve gösterileceği bağlama göre uygulanır.

Kural çift kodlamayı da önler. Değeri hem kayıtta hem gösterimde kodlarsan ekranda &amp;lt; gibi bozuk bir metin görürsün. Çift kodlama tek başına açık değildir. Ama ekipleri kodlamayı tümden kaldırmaya iter, açık da o zaman doğar.

Etkinlik 2 / 7

Aşağıdaki durum sayfası kodunda XSS'e açık satırı bul.

Zafiyetli satıra tıkla.

Bağlam tablosu ve URL bağlamı

HTML gövdesi, kullanıcı verisinin basılabileceği bağlamlardan yalnız biridir. Kodlama kuralı bağlama göre değişir. HTML özniteliği içinde tırnak işareti de etiket karakterleri kadar önemlidir.

JavaScript dizesi içine basarken farklı bir kaçış kuralı, URL parametresi içinde ise yüzde kodlaması geçerlidir. Tek bir kaçış fonksiyonu bu bağlamlara birden yetmez. Modern arayüz kütüphaneleri, metni HTML gövdesine basarken varsayılan olarak otomatik kodlar.

Risk genelde ham HTML basan özel fonksiyonlarda ortaya çıkar. Bağlamları karıştırmak, bir yerde fazladan kodlama yapıp veriyi bozmaya, başka bir yerde hiç kodlamayıp açık bırakmaya yol açar. Aşağıdaki tablo her bağlam için bir örnek girdiyi ve kodlanmış halini gösterir.

Bağlam                    Örnek girdi        Kodlanmış çıktı
HTML gövdesi              <b>Ezik</b>        &lt;b&gt;Ezik&lt;/b&gt;
Çift tırnaklı öznitelik   5" kutu            5&quot; kutu
Tırnaksız öznitelik       kullanma: değeri her zaman tırnak içine al
JavaScript dizesi         '</script>         \x27\x3c\x2fscript\x3e
URL parametresi           iade durumu&x=1    iade%20durumu%26x%3D1
CSS özellik değeri        red;}              red\3b \7d
Bağlam tablosu: aynı karakter her bağlamda başka bir biçimde kodlanır.

Tabloyu satır satır oku. HTML gövdesinde etiket açan karakterler varlığa döner ve tarayıcı onları metin olarak gösterir. Öznitelikte asıl tehlike tırnaktır. Kapanmayan bir tırnak, saldırganın yeni bir öznitelik, örneğin bir olay işleyicisi eklemesine yol açar.

Tırnaksız öznitelik kullanma, çünkü orada bir boşluk bile özniteliği bitirir. JavaScript dizesinde OWASP harf ve rakam dışındaki her karakteri onaltılık kaçışla yazmayı önerir. Ters bölüyle tırnak kaçışlamak yetmez, çünkü </script> dizisi betik bloğunu HTML düzeyinde kapatır. Aynı tuzak JSON gömerken de çıkar.

JSON.stringify bu diziyi olduğu gibi bırakır, bu yüzden gömmeden önce < karakterini \u003c ile değiştir. URL parametresinde boşluk %20, ve işareti %26 olur. CSS'te kullanıcı değerini yalnız bir özellik değerine koy ve kodla.

URL bağlamı ayrı bir dikkat ister. Müşterinin girdiği bir adresi href içine koyduğunu düşün. HTML kodlaması tırnağı kaçışlar ama javascript: ile başlayan bir adresi durdurmaz. Bağlantıya tıklanınca tarayıcı adresin kalanını betik olarak çalıştırır.

React 19 bu şemayı href ve src değerlerinde engeller. React 18 ve öncesi yalnız geliştirme modunda konsola uyarı yazar. Kütüphaneye güvenmek yerine şemayı kendin denetle. Adresi URL ayrıştırıcısıyla çöz ve protokolü bir izin listesiyle karşılaştır: yalnız http: ve https:.

Metin karşılaştırması yapma. Tarayıcı büyük harfli JAVASCRIPT: yazımını da, başa eklenen boşluk ve sekme karakterlerini de kabul eder. Ayrıştırıcı ise hepsini aynı biçime indirir.

function guvenliAdres(girdi) {
  try {
    const url = new URL(girdi, "https://iade.altaykargo.example");
    return ["http:", "https:"].includes(url.protocol) ? url.href : "#";
  } catch {
    return "#";
  }
}
// " JAVASCRIPT:..."  -> protocol "javascript:" -> "#"
// "/durum/AK482913"  -> protocol "https:"      -> tam adres
Şema izin listesi: adres önce ayrıştırılır, sonra protokolü denetlenir.

Etkinlik 3 / 7

Her bağlamı doğru kodlama kuralıyla eşleştir.

Soldan bir tane seç, sağdan eşini seç. Kullanılmış bir seçeneği yeniden seçersen yeni öğeye taşınır.

Şablon motorları ve zengin metin

Modern şablon motorlarının çoğu HTML gövdesinde kodlamayı kendisi yapar. React'te {aciklama} yazımı metni düz metin olarak basar. Vue'da {{ aciklama }} aynı işi görür.

Jinja2'de otomatik kaçış (autoescape) açıksa {{ aciklama }} kodlanır. Ama her motorun bir kaçış kapısı vardır: React'te dangerouslySetInnerHTML, Vue'da v-html, Jinja2'de |safe süzgeci. Üçü de motora bu metne güvendiğini ve kodlamamasını söyler. Kod incelemesinde önce bu adları ara.

Jinja2'de bir tuzak daha var. Environment nesnesinin varsayılan autoescape değeri False'tur. Flask gibi çatılar .html uzantılı şablonlarda kaçışı kendisi açar. Kendi Environment nesneni kuruyorsan select_autoescape() ile açıkça açman gerekir.

from jinja2 import Environment, FileSystemLoader, select_autoescape

env = Environment(
    loader=FileSystemLoader("sablonlar"),
    autoescape=select_autoescape(),   # html, htm ve xml şablonlarında açık
)

# durum.html içinde:
# <p>{{ iade.aciklama }}</p>          kodlanır
# <p>{{ iade.aciklama | safe }}</p>   kodlanmaz, XSS açığı
Jinja2: otomatik kaçışı açıkça aç, |safe süzgecini kullanıcı verisinde kullanma.

Bazen düz metin yetmez. Destek ekibi iade notlarında kalın yazı ve liste kullanmak ister. Kodlama burada işe yaramaz, çünkü bütün etiketleri metne çevirir.

Doğru araç temizlemedir (sanitization). DOMPurify gibi bakımlı bir kütüphane HTML'i ayrıştırır ve yalnız listelediğin etiketleri bırakır. Kendi temizleme filtreni yazma.

Tarayıcıların HTML ayrıştırma kuralları karmaşıktır ve el yapımı regex filtreleri defalarca atlatılmıştır. Kütüphaneyi de güncel tut, çünkü atlatma yolları bulundukça yeni sürümler çıkar. Markdown kullanıyorsan oluşturucunun ham HTML ayarına bak. Bazı Markdown kütüphaneleri metindeki HTML'i varsayılan olarak olduğu gibi geçirir.

import DOMPurify from "dompurify";

const temiz = DOMPurify.sanitize(not, {
  ALLOWED_TAGS: ["b", "i", "p", "ul", "li"],
  ALLOWED_ATTR: [],
});
kutu.innerHTML = temiz;
Zengin metin: yalnız beş etiket kalır, bütün öznitelikler silinir.

Etkinlik 4 / 7

React'te normal {aciklama} yazımıyla basılan bir metin ile aynı bileşende dangerouslySetInnerHTML={{__html: aciklama}} kullanmak arasındaki fark nedir?

Etkinlik 5 / 7

Doğru mu, yanlış mı?

Bir e-posta şablonunda kullanıcı adını HTML gövdesine basarken kullandığın kodlama fonksiyonu, aynı adı URL sorgu parametresine eklerken de doğrudan kullanılabilir.

CSP ve Trusted Types: ikinci savunma katmanı

Kodlama tek savunma katmanı değildir. Bir geliştirici bir gün bir yerde kodlamayı unutur. İçerik güvenlik politikası (Content Security Policy, CSP) o güne hazırlıktır.

Sunucu yanıtla birlikte bir başlık gönderir ve tarayıcıya hangi betiklerin çalışabileceğini söyler. Eski yaklaşım alan adlarını tek tek listelemekti. Bu listeler zamanla şişer ve izinli bir alandaki eski bir kütüphane üzerinden atlatılır.

OWASP'ın önerdiği katı politika (strict CSP) alan adı yerine tek kullanımlık bir değere, yani nonce'a dayanır. Sunucu her yanıtta rastgele bir nonce üretir, başlığa ve kendi script etiketlerine yazar. Saldırganın sayfaya soktuğu betik bu değeri bilmez ve çalışmaz.

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

<script nonce="kV3pQ9xR2m" src="/js/iade.js"></script>
<script>/* nonce yok: tarayıcı çalıştırmaz */</script>
Nonce tabanlı katı CSP. Değer her yanıtta yeniden üretilir.

Başlığı parça parça oku. script-src yalnız doğru nonce taşıyan betiği çalıştırır. 'strict-dynamic' güvenilen bir betiğin sonradan yüklediği betiklere de güveni aktarır.

object-src 'none' eski eklenti nesnelerini, base-uri 'none' ise göreli adresleri başka sunucuya yönelten base etiketini kapatır. Nonce her yanıtta yeniden üretilir, sabit bir nonce hiç nonce olmamasıyla aynıdır. Bütün script etiketlerine otomatik nonce basan bir ara katman da yazma, çünkü o katman saldırganın etiketine de nonce basar.

Politikayı üretime doğrudan koyma. Önce Content-Security-Policy-Report-Only başlığıyla yayına al. Bu modda tarayıcı hiçbir şeyi engellemez, yalnız ihlalleri report-to yönergesinin gösterdiği adrese bildirir.

Raporları okuyup meşru betiklere nonce ekledikten sonra başlığı zorlayıcı moda çevir. Başlığın gönderildiğini curl -sI ile doğrularsın: -I yalnız başlıkları ister, -s ilerleme çıktısını gizler. ZAP'ın 10038 numaralı kuralı başlık hiç yoksa orta riskli bir bulgu üretir ve onu CWE-693 ile eşler.

CSP betiğin kaynağını denetler. Trusted Types ise DOM tabanlı XSS'in hedeflerini kapatır. require-trusted-types-for 'script' yönergesi açıkken innerHTML gibi bir hedefe düz metin atamak TypeError fırlatır.

Hedef yalnız bir politika fonksiyonundan geçmiş TrustedHTML nesnesini kabul eder. Politikanın içine DOMPurify koyarsın, böylece sayfadaki bütün HTML yazımları tek bir denetim noktasından geçer. Kod incelemesi de kolaylaşır.

Yüzlerce innerHTML satırı yerine tek bir politikayı incelersin. MDN, API'nin Şubat 2026'dan beri güncel tarayıcıların hepsinde desteklendiğini belirtir. Eski tarayıcılar için küçük bir yedek kod (tinyfill) önerir.

// Yanıt başlığı: Content-Security-Policy: require-trusted-types-for 'script'
const politika = trustedTypes.createPolicy("iade-notu", {
  createHTML: (girdi) => DOMPurify.sanitize(girdi),
});
kutu.innerHTML = politika.createHTML(not);  // TrustedHTML: kabul edilir
kutu.innerHTML = not;                       // düz metin: TypeError
Trusted Types: tehlikeli hedefler yalnız politikadan geçmiş değeri kabul eder.
Derinleş:CSP neden alan adı listesinden nonce'a geçti

CSP ilk yıllarında bir alan adı izin listesi olarak kullanıldı. Politika, betiklerin yalnız belirli alan adlarından yüklenebileceğini söylüyordu. 2016'da dört araştırmacı bu yaklaşımı büyük ölçekte ölçtü ve bulguları ACM CCS konferansında yayımladı. Çalışma, bir milyarı aşkın host adını kapsayan bir arama motoru verisinde CSP kullanan yaklaşık 1,68 milyon hostu ve 26.011 farklı politikayı inceledi.

Farklı politikaların yüzde 94,72'si atlatılabilir çıktı. En sık izin verilen 15 betik alanından 14'ünde güvensiz bir uç nokta vardı. Örneğin geri çağrı adı parametreyle belirlenen bir JSONP ucu ya da eski bir çatı kütüphanesi. Saldırgan kendi betiğini yazmak yerine izinli alandaki bu uçları kullanıyordu.

Araştırmacılar çözüm olarak nonce tabanlı politikayı ve 'strict-dynamic' anahtar kelimesini önerdi. Bugün OWASP'ın katı CSP önerisi doğrudan bu çalışmaya dayanır. Ders açıktır: güveni bir alan adına bağlamak, o alandaki her şeye güvenmek demektir.

Etkinlik 6 / 7

Lab ortamındaki durum sayfasının yalnız yanıt başlıklarını, ilerleme çıktısı olmadan getir. Adres: https://iade.altaykargo.example/durum/AK482913

ogrenci@lab

Etkinlik 7 / 7

Nonce tabanlı katı bir CSP kurarken hangileri doğru uygulamadır?

Birden fazla doğru var.

Bu derste bir hata mı gördün? Bildir