Bölüm 05
Çıktı Kodlama (Output Encoding)
Bölüm 1 (güven sınırı) ve Bölüm 4 (doğrulamanın ne olmadığı).
"Kaçış" (escaping) ve "kodlama" (encoding) kavramlarını bir bağlam problemi olarak kavrayacak; aynı verinin HTML, HTML-öznitelik, JavaScript, URL ve CSS bağlamlarında neden farklı biçimde kodlanması gerektiğini göreceksiniz. Bu bölüm, XSS (Bölüm 7) bölümünün teorik temelidir.
1. Giriş
Bölüm 4'te girdi doğrulamanın enjeksiyonu tek başına çözmediğini gördük. Peki asıl çözüm nerede? Cevap: çıktı anında, hedef bağlama uygun kodlamada.
Buradaki içgörü, SQL Injection bölümüyle (Bölüm 6) tam olarak aynıdır: sorun, verinin kod olarak yorumlanmasıdır. SQL'de veri, veritabanı ayrıştırıcısının komutu sanmasıyla sorun olur; HTML/JS'de veri, tarayıcının işaretleme (markup) veya kod sanmasıyla sorun olur. Her iki durumda da çözüm, veriyi hedef ayrıştırıcının "bu saf veridir, komut değildir" diye anlayacağı biçime dönüştürmektir.
Çıktı kodlama, bu dönüştürmedir. Ve en kritik gerçeği şudur: doğru kodlama, verinin nereden geldiğine değil, nereye gittiğine bağlıdır. Aynı kullanıcı adı, bir HTML gövdesine yazılırken bir türlü, bir <a href> özniteliğine yazılırken başka türlü, bir <script> bloğuna yazılırken bambaşka türlü kodlanır. Tek bir "güvenli hale getir" fonksiyonu yoktur; bağlam sayısı kadar kodlama vardır.
2. Temel Teori: Bağlam Neden Belirleyici?
Bir tarayıcı, bir HTML belgesini işlerken birden çok ayrıştırıcı kullanır: HTML ayrıştırıcısı, sonra bazı yerlerde JavaScript ayrıştırıcısı, CSS ayrıştırıcısı, URL çözümleyici. Her ayrıştırıcının kendi özel karakterleri ve kaçış kuralları vardır. Verinin "tehlikeli" olup olmadığı, hangi ayrıştırıcının onu okuyacağına bağlıdır.
Beş temel çıktı bağlamı ve her birinin "tehlikeli" karakterleri:
Bağlam Örnek konum Kritik karakterler / kaçış
─────────────────────────────────────────────────────────────────────────
HTML gövdesi <div>BURAYA</div> < > & " ' → HTML entity
HTML özniteliği <input value="BURAYA"> " ' ve tırnaksızsa boşluk/=
JavaScript <script>x="BURAYA"</... " ' \ </ satır sonları
URL / query <a href="?q=BURAYA"> RFC 3986 yüzde-kodlama
CSS style="color:BURAYA" ( ) ; { } ve ifade enjeksiyonu
Kritik ders: bir bağlam için doğru kodlama, başka bir bağlamda yetersiz ya da yanlıştır. HTML-entity kodlaması, bir href="javascript:..." içindeki javascript: şemasını durdurmaz. JavaScript kaçışı, HTML ayrıştırıcısının </script> dizisini erken kapatmasını engellemez. Bu yüzden kodlama daima hedef bağlama göre seçilir.
3. Mimarisel Bakış
Verinin girişten çıkışa yolculuğu ve kodlamanın nerede yapıldığı:
[Kullanıcı girdisi]
↓ ╳ güven sınırı (giriş)
[ Doğrulama (Böl. 4) ] → biçim sözleşmesi; ama HÂLÂ metin, güvenli değil
↓
[ Depolama: DB ] → HAM olarak saklanır (kodlanmadan!)
↓
[ İş mantığı ]
↓ ╳ güven sınırı (çıkış) ← KODLAMA TAM BURADA
[ Bağlama göre kodla ] → HTML? attr? JS? URL? CSS?
↓
[ Tarayıcı ayrıştırıcısı ] → veriyi SAF VERİ olarak görür, kod olarak değil
Önemli tasarım kararı: Veri depoya kodlanmadan, ham hâliyle yazılır; kodlama yalnızca çıktı anında ve o çıktının bağlamına göre yapılır. Depoda kodlamak ("kaydederken escape et") yaygın bir hatadır: aynı veri farklı bağlamlarda (HTML, e-posta, JSON API, PDF) kullanılacağı için, tek bir kodlamayla saklamak ya bir bağlamda yetersiz kalır ya da diğerinde çift kodlamaya (&amp;) yol açar.
4. Güvensiz Kod
Gerçekçi bir kullanıcı profili görüntüleme parçası:
<?php
// ⚠️ GÜVENSİZ — kodlama yok / yanlış bağlam
$name = $user['display_name']; // kullanıcı kontrollü
$bio = $user['bio']; // kullanıcı kontrollü, çok satırlı
$website = $user['website']; // kullanıcı kontrollü URL
$theme = $user['theme_color']; // kullanıcı kontrollü renk
?>
<h1><?= $name ?></h1>
<a href="<?= $website ?>">Web sitesi</a>
<div style="color: <?= $theme ?>"><?= $bio ?></div>
<script>
var currentUser = "<?= $name ?>";
</script>
Bu şablon dört farklı bağlamda dört farklı XSS açığı barındırır.
5. Açığın Analizi
<h1><?= $name ?></h1> — HTML gövde bağlamı, hiç kodlama yok. $name içine <img src=x onerror=...> gibi bir işaretleme konulursa tarayıcı onu HTML olarak ayrıştırır → depolanmış XSS.
<a href="<?= $website ?>"> — HTML öznitelik bağlamı. İki katmanlı sorun: (1) " ile öznitelikten çıkılabilir; (2) çıkılmasa bile href="javascript:alert(1)" şema-tabanlı XSS'tir. HTML-entity kodlaması ikinciyi durdurmaz; ayrıca şema beyaz listesi gerekir.
style="color: <?= $theme ?>" — CSS bağlamı. HTML kodlaması burada yetersiz; CSS ifade enjeksiyonu ve öznitelikten çıkış mümkün. CSS bağlamı en zor kodlanan bağlamlardan biridir; en iyi çözüm, kullanıcıya serbest CSS bırakmamaktır (renk için beyaz liste / doğrulanmış hex).
var currentUser = "<?= $name ?>"; — JavaScript dize bağlamı. " ile dizeden, </script> ile tüm script bloğundan çıkılabilir. HTML-entity kodlaması JS bağlamında yanlıştır: " JS dizesinde tırnağı temsil etmez, veriyi bozar ama korumaz.
Kök sorun: tek bir bağlam-körü yaklaşım (ya hiç kodlama ya da her yere htmlspecialchars) dört bağlamın hiçbirini doğru karşılamaz.
6. Hacker Bakış Açısı
Saldırgan XSS ararken (Bölüm 7'de derinleşir) tam olarak bağlam peşindedir:
Girdiyi yansıtan yerleri arar. Verdiği bir değeri sayfada geri görüyorsa, o değerin hangi bağlamda yansıdığını inceler: gövdede mi, öznitelikte mi, script içinde mi?
Bağlamdan çıkmayı dener. Öznitelik bağlamındaysa bir ", script bağlamındaysa </script> veya bir tırnak, HTML gövdesindeyse bir < göndererek "ayrıştırıcının modunu değiştirebiliyor muyum?" diye yoklar.
Kodlamanın bağlam-uyumunu test eder. Uygulama htmlspecialchars uygulamış ama saldırgan JS bağlamındaysa, HTML kodlamasının orada işe yaramadığını bilir ve tırnak/</script> ile ilerler. Yani yanlış kodlama, kodlama yokluğu kadar sömürülebilirdir.
Savunmacı dersi: Saldırgan bağlam düşünür; savunma da bağlam düşünmelidir. "Escape ettik" demek yetmez — "hangi bağlama göre?" sorusu belirleyicidir.
7. Güvenli Kod
Her bağlam için doğru kodlama:
<?php
// ✅ GÜVENLİ — bağlama özel kodlama
declare(strict_types=1);
/** HTML gövde ve öznitelik için (çift tırnak dahil, UTF-8) */
function h(string $s): string {
return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
/** URL şema beyaz listesi: yalnızca http/https/mailto */
function safeUrl(string $url): string {
$scheme = strtolower(parse_url($url, PHP_URL_SCHEME) ?? '');
return in_array($scheme, ['http', 'https', 'mailto'], true) ? $url : '#';
}
$name = $user['display_name'];
$bio = $user['bio'];
$website = $user['website'];
$theme = $user['theme_color'];
// Renk: serbest CSS yerine beyaz-listeli / doğrulanmış hex (Bölüm 4)
$safeTheme = preg_match('/^#[0-9a-fA-F]{6}$/', $theme) ? $theme : '#333333';
?>
<h1><?= h($name) ?></h1>
<a href="<?= h(safeUrl($website)) ?>">Web sitesi</a>
<div style="color: <?= $safeTheme ?>"><?= nl2br(h($bio)) ?></div>
<script>
// JS bağlamı: veriyi JSON olarak göm — JS kodlamasını json_encode üstlenir
var currentUser = <?= json_encode($name, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>;
</script>
Öne çıkan modern PHP kalıpları:
- HTML/öznitelik: htmlspecialchars daima ENT_QUOTES (tek+çift tırnak) ve 'UTF-8' ile. ENT_SUBSTITUTE, geçersiz baytları bozuk çıktı yerine güvenli yer tutucuyla değiştirir.
- JavaScript: Elle kaçış yerine json_encode. JSON_HEX_* bayrakları <, >, &, ', " karakterlerini \u00XX'e çevirerek hem </script> çıkışını hem HTML/JS karışımını kapatır. Bu, JS bağlamında en sağlam yöntemdir.
- URL: Şema beyaz listesi (safeUrl) + öznitelik kodlaması birlikte.
- CSS: Serbest CSS'ten kaçının; renk gibi değerleri doğrulanmış beyaz listeye indirin.
Şablon motorları: Twig ve Blade otomatik çıktı kodlaması (auto-escaping) yapar — Twig varsayılan {{ }}, Blade {{ }} HTML kodlar. Bu, en güvenilir savunmadır çünkü "kodlamayı unutma" hatasını ortadan kaldırır. Ancak {!! !!} (Blade) veya |raw (Twig) kaçış kapıları auto-escaping'i devre dışı bırakır ve kod incelemede özellikle işaretlenmelidir. Auto-escaping de HTML bağlamına göredir; JS/CSS/URL bağlamlarında ek özen gerekir.
8. Patch Analizi
Neden işe yarar? Her bağlam, o bağlamın ayrıştırıcısının "saf veri" olarak göreceği biçime dönüştürülür. HTML'de < → < olduğunda ayrıştırıcı bir etiket başlangıcı görmez. JS'de json_encode ile </script> → \u003c/script> olduğunda HTML ayrıştırıcısı bloğu erken kapatmaz. Şema beyaz listesi, javascript: gibi kod-taşıyan şemaları en baştan eler.
Alternatif karşılaştırması:
| Bağlam | Yanlış yaygın çözüm | Doğru çözüm |
|---|---|---|
| HTML gövde | Hiçbiri / strip_tags |
htmlspecialchars(ENT_QUOTES) |
| HTML attr | Yalnızca <> kodlama |
htmlspecialchars(ENT_QUOTES) + tırnaklı |
| JavaScript | htmlspecialchars (yanlış!) |
json_encode + JSON_HEX_* |
| URL | Ham gömme | Şema beyaz listesi + öznitelik kodlama |
| CSS | Elle kaçış | Beyaz liste / hiç serbest CSS verme |
Performans: Kodlama fonksiyonları çok hızlıdır; etkisi ölçülemeyecek düzeydedir. Auto-escaping'in derleme aşamasında yapılması (Blade/Twig) çalışma zamanı maliyetini de en aza indirir.
9. Katmanlı Savunma: CSP
Çıktı kodlaması ilk ve en önemli savunmadır, ama tek başına bırakılmamalıdır. Content Security Policy (CSP) bir HTTP başlığıdır ve tarayıcıya "hangi kaynaklardan script çalıştırabilirsin" der. İyi yapılandırılmış bir CSP (satır içi script'i yasaklayan, nonce/hash tabanlı), bir kodlama hatası kodun içine sızsa bile onun çalışmasını engelleyebilir. Bu, Bölüm 1'deki derinlemesine savunmanın somut örneğidir: kodlama düşerse CSP tutar. CSP ayrıntısı Bölüm 7 (XSS) içinde ele alınır.
10. Gerçek Hayat Senaryosu
Kurgu — bir müşteri destek (helpdesk) sistemi. Müşteriler destek talebi açarken bir "konu" ve "mesaj" girer. Bu içerik, destek ekibinin admin panelinde görüntülenir. Geliştirici, "müşteri verisi zaten doğrulanıyor" diye admin görünümünde çıktı kodlamasını atlar. Bir saldırgan, mesaj alanına depolanmış bir XSS yükü koyar. Yük, bir destek temsilcisi talebi açtığında onun oturumunda çalışır — yani saldırgan, düşük yetkili bir müşteri olarak, yüksek yetkili bir admin oturumunu ele geçirir (privilege escalation). Bu senaryo iki dersi birleştirir: (1) doğrulama kodlamanın yerine geçmez; (2) çıktı kodlaması tüketildiği her bağlamda gereklidir — özellikle yüksek ayrıcalıklı görünümlerde.
11. Gerçek CVE Örneği
Çıktı kodlama eksikliğinin gerçek dünya karşılığı, kayıtlı XSS CVE'lerinin devasa yığınıdır (CWE-79). Somut ve doğrulanabilir bir aile, WordPress eklentilerinde yıllar boyunca yayımlanan stored/reflected XSS advisory'leridir: bir eklenti, kullanıcı girdisini (ör. form alanı, ayar değeri) yönetici panelinde kodlamadan yazdığında, saldırgan depolanmış XSS ile admin oturumunu hedefler. Bu ailenin ortak kök nedeni her seferinde aynıdır: çıktı, tüketildiği bağlama göre kodlanmamıştır.
Bu genel bir örüntüdür; belirli bir eklenti/sürüm için WPScan ve resmî CVE kaydını doğrulayın. Ayrıntılı vaka analizi Bölüm 7 ve 35'te.
12. Detection
Kod incelemede: Şablonlarda <?= $... ?>, echo $... gibi kodlanmamış çıktıları arayın. Auto-escaping kullanan projelerde {!! !!} / |raw kaçış kapılarını denetleyin. Her çıktı için "hangi bağlam ve doğru kodlama mı?" sorusunu sorun.
grep -rnE '<\?=\s*\$' templates/ # kodlanmamış olabilir
grep -rn '{!!' resources/ # Blade raw çıktı
grep -rn '| *raw' templates/ # Twig raw
SAST: Taint analizi, girdiden şablon çıktısına uzanan kodlanmamış yolları gösterir; XSS kuralları olgundur.
DAST: Yansıyan/depolanan girdileri bağlam-farkında yüklerle deneyerek kodlama boşluklarını bulur.
Loglar/SIEM: Çıktı-tarafı XSS log izi zayıftır; girişte enjeksiyon-benzeri kalıplar (<, onerror, javascript:) ve CSP ihlal raporları (report-uri) izlenmelidir.
13. Prevention
- Auto-escaping şablon motoru kullanın (Twig/Blade); elle kodlamayı kural değil istisna yapın.
- Bağlama göre kodla: HTML/attr için
htmlspecialchars(ENT_QUOTES), JS içinjson_encode, URL için şema beyaz listesi. - Ham çıktı kapılarını (
|raw,{!! !!}) yasak-varsayılan yapın; kullanımı gözden geçirilir. - Depoda değil, çıktıda kodla; veriyi ham sakla.
- CSP ile ikinci katmanı ekleyin.
- Kullanıcıya HTML izni veriyorsanız (zengin metin), asla elle kodlamayla değil, sağlam bir HTML sanitizer ile (ör. HTML Purifier) temizleyin.
14. Mitigation
- Acil: kodlanmamış çıktının bulunduğu şablonlara bağlam-doğru kodlama ekleyin; kritik yüzeylerde geçici olarak zengin içeriği düz metne indirin.
- Geçici: sıkı bir CSP devreye alarak mevcut kodlama boşluklarının çalışmasını engelleyin; depolanmış XSS için etkilenen kayıtları tarayıp temizleyin.
- Kalıcı: auto-escaping'e geçiş + ham kapıların denetimi + testler.
15. Checklist
- [ ] Her dinamik çıktı, tüketildiği bağlama göre mi kodlanıyor?
- [ ]
htmlspecialcharsdaimaENT_QUOTES+'UTF-8'ile mi? - [ ] JS bağlamında elle kaçış yerine
json_encodemı kullanılıyor? - [ ]
href/srcgibi URL bağlamlarında şema beyaz listesi var mı? - [ ] Auto-escaping açık mı ve
raw/{!! !!}kapıları denetleniyor mu? - [ ] Veri depoya ham mı yazılıyor (çift kodlama önlemi)?
- [ ] Zengin metin bir sanitizer'dan (HTML Purifier vb.) mı geçiyor?
- [ ] İkinci katman olarak CSP tanımlı mı?
16. Laboratuvar
Lab 5.1 — Bağlam matrisi. Tek bir kullanıcı adını ("><script> içeren) HTML gövde, öznitelik, JS ve URL bağlamlarına yazdırın (izole ortam). Her bağlamda htmlspecialchars'ın davranışını gözlemleyin; JS bağlamında neden yetersiz kaldığını gösterin ve json_encode ile düzeltin.
Lab 5.2 — Auto-escaping kapısı. Bir Twig/Blade şablonunda auto-escaping'i gözlemleyin, sonra |raw/{!! !!} ile devre dışı bırakıp aynı yükün nasıl çalıştığını görün. Kaçış kapısının riskini belgeleyin.
Lab 5.3 — CSP. Bir sayfaya satır içi script'i yasaklayan bir CSP başlığı ekleyin ve kodlanmamış bir yükün artık çalışmadığını (konsolda CSP ihlali) gözlemleyin.
17. Quiz
- Doğru çıktı kodlaması neden verinin kaynağına değil hedefine bağlıdır?
- Aynı veri HTML gövde ve JavaScript bağlamlarında neden farklı kodlanır?
htmlspecialcharsbir JS dize bağlamında neden yanlış/yetersizdir?- JS bağlamında
json_encodeneden en sağlam çözümdür?JSON_HEX_*bayrakları ne yapar? - HTML-entity kodlaması
href="javascript:..."XSS'ini neden durdurmaz? - Neden veri depoya kodlanarak değil, ham saklanmalı; kodlama çıktıda yapılmalıdır?
- Auto-escaping nedir ve
|raw/{!! !!}neden risklidir? ENT_QUOTESbayrağı olmadanhtmlspecialcharshangi bağlamda açık bırakır?- CSP, çıktı kodlamasının yerini alır mı yoksa tamamlar mı? Neden?
- Zengin metin (kullanıcı HTML'i) için neden elle kodlama değil, sanitizer gerekir?
18. Kaynakça
- OWASP, Cross Site Scripting Prevention Cheat Sheet (bağlamsal kodlama kuralları).
- OWASP, DOM based XSS Prevention Cheat Sheet.
- MITRE, CWE-79: Improper Neutralization of Input During Web Page Generation; CWE-116: Improper Encoding or Escaping of Output.
- PHP Manual,
htmlspecialchars,json_encode(JSON_HEX_* bayrakları) referansları. - W3C / MDN, Content Security Policy (CSP) spesifikasyonu.
- Twig ve Laravel Blade dokümantasyonu, auto-escaping bölümleri.