
PHP'de XSS ve Güvenli Çıktı Üretimi
Siteler arası betik açığı, saldırganın gönderdiği metnin tarayıcıda kod olarak çalışmasıyla oluşur. Sonuçları oturum çalmaktan sayfa içeriğini değiştirmeye kadar uzanır. Açığın kaynağı çoğu zaman veriyi kaydeden kod değil, veriyi ekrana basan koddur.
Yaygın ama yanlış bir yaklaşım, gelen veriyi kaydederken temizlemektir. Aynı veri farklı yerlerde farklı biçimlerde kullanılır: HTML gövdesinde, bir öznitelik içinde, adres satırında ya da JavaScript değişkeninde. Doğru kodlama ancak kullanım anında, bağlam bilindiğinde yapılabilir.
Açığın üç biçimi
- Yansıyan: istekle gelen değer aynı yanıtta ekrana basılır. Arama sonucu sayfalarında sık görülür.
- Saklı: değer veritabanına kaydedilir ve her görüntülemede çalışır. Yorum alanları tipik örnektir.
- Belge nesnesi tabanlı: sunucu yanıtı temiz olsa da tarayıcıdaki betik, adres satırından aldığı değeri sayfaya yazar.
Üçü de aynı temel kuralla önlenir: veri, hangi bağlama yazılıyorsa o bağlamın kodlamasından geçirilir.
HTML gövdesinde kaçış
En sık kullanılan kodlama, özel karakterleri varlık karşılıklarına çeviren fonksiyondur. Varsayılan parametrelerle çağrılması yeterli değildir; tek tırnaklar ve geçersiz bayt dizileri açıkça ele alınmalıdır.
function e(?string $deger): string
{
return htmlspecialchars(
$deger ?? '',
ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5,
'UTF-8',
);
}
echo '<p>' . e($yorum) . '</p>';ENT_QUOTES hem tek hem çift tırnağı kodlar; öznitelik değerleri tek tırnakla yazıldığında bu şarttır. ENT_SUBSTITUTE geçersiz karakter dizilerini boş çıktı yerine yedek karakterle değiştirir; aksi halde fonksiyon boş metin döndürür ve içerik sessizce kaybolur.
Bağlama göre kodlama
HTML kaçışı her yerde yeterli değildir. Değerin yazıldığı yer değiştikçe kullanılacak kodlama da değişir.
// öznitelik: tırnak içinde ve kaçırılmış olmalı
echo '<a title="' . e($baslik) . '">';
// adres bileşeni
echo '<a href="/ara?q=' . urlencode($terim) . '">';
// betik içine veri gömmek: json_encode ile, ek bayraklarla
$json = json_encode($veri, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
| JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
echo '<script>const ayar = ' . $json . ';</script>';Bazı bağlamlara kullanıcı verisi hiç yazılmamalıdır. Adresin protokol bölümü, olay öznitelikleri ve stil tanımları bunların başında gelir. Kullanıcıdan gelen bir adres bağlantıya dönüştürülecekse, protokolün beklenen değerlerden biri olduğu doğrulanmalıdır.
$parcalar = parse_url($adres);
if (!in_array($parcalar['scheme'] ?? '', ['http', 'https'], strict: true)) {
throw new InvalidArgumentException('Desteklenmeyen adres');
}Şablon katmanını ayırmak
Kaçışın unutulmasının en yaygın nedeni, iş mantığı ile görüntüleme kodunun aynı dosyada bulunmasıdır. Görüntüleme ayrı dosyalara alındığında, o dosyalarda yalnızca değişken basma işlemi kalır ve kaçış kuralı tek bir alışkanlığa iner.
<!-- gorunum/yazi.php -->
<article>
<h1><?= e($yazi['baslik']) ?></h1>
<p><?= e($yazi['ozet']) ?></p>
</article>// şablonu yalıtılmış biçimde çalıştıran yardımcı
function goster(string $dosya, array $veri): string
{
extract($veri, EXTR_SKIP);
ob_start();
require __DIR__ . '/gorunum/' . $dosya;
return (string) ob_get_clean();
}Kendi kendine kaçış yapan bir şablon motoru kullanmak da geçerli bir yoldur. Bu durumda kaçışın kapatıldığı yerlerin sayısı sınırlı tutulmalı ve her biri gözden geçirilmiş olmalıdır.
Kullanıcıdan gelen HTML
Zengin metin girişine izin veren alanlarda kaçış yapılamaz, çünkü etiketlerin çalışması istenir. Bu durumda içerik, izin verilen etiket ve öznitelik listesine göre temizlenir. Temizleme işi düzenli ifadeyle yapılmaz; bu yolla yazılan süzgeçler yıllardır düzenli olarak aşılmaktadır.
Bakımlı bir temizleme kütüphanesi kullanmak, izin listesini dar tutmak ve sonucu güvenilir kabul etmemek gerekir. Kaydedilen içerik gösterilirken de içerik güvenlik politikası ikinci bir savunma katmanı oluşturur.
// izin listesi yaklaşımı
$izinli = ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'a', 'h2', 'h3'];
$temiz = $temizleyici->temizle($ham, etiketler: $izinli, ozellikler: ['href', 'title']);Doğrulama ile kodlamayı ayırmak
Girdi doğrulama ve çıktı kodlaması farklı işlerdir ve birbirinin yerine geçmez. Doğrulama, verinin iş kurallarına uygun olup olmadığını sorgular: e-posta biçiminde mi, uzunluk sınırında mı, izin verilen değerlerden biri mi. Kodlama ise verinin yazılacağı bağlamda zararsız kalmasını sağlar.
Doğrulamada başarısız olan veri reddedilir, değiştirilerek kabul edilmez. Zararlı görünen bölümleri silerek veriyi kurtarmaya çalışan süzgeçler, hem geçerli içeriği bozar hem de atlatılabilir. Reddetme, sessiz düzeltmeye her zaman tercih edilir.
// doğrulama: kural sağlanmıyorsa reddedilir
if (!in_array($tur, ['bireysel', 'kurumsal'], strict: true)) {
throw new InvalidArgumentException('Geçersiz tür');
}
// kodlama: veri geçerli olsa da çıktıda kaçırılır
echo '<span>' . e($tur) . '</span>';Farklı çıktı biçimleri
Kaçış kuralı çıktının biçimine göre değişir. JSON yanıtı üreten bir uçta HTML kaçışı yapılmaz; veri olduğu gibi kodlanır ve tüketen taraf kendi bağlamına göre işler. HTML kaçırılmış metnin JSON içine konması, arayüzde varlık kodlarının görünmesine yol açar.
E-posta içeriğinde de aynı ayrım geçerlidir. HTML biçimli e-postalarda kaçış gerekir, düz metin biçiminde gerekmez. Aynı verinin iki biçimde de gönderildiği durumlarda kaçış, biçim üretilirken uygulanır; ortak bir yerde önceden uygulanmaz.
İçerik güvenlik politikası
Bu başlık, tarayıcıya hangi kaynaklardan betik çalıştırabileceğini söyler. Doğru kurulduğunda, gözden kaçan bir açık bile betiğin çalışmasına yetmez.
header("Content-Security-Policy: default-src 'self'; "
. "script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'");
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');Satır içi betiklere izin veren bir politika koruma sağlamaz. Betiklerin ayrı dosyalara taşınması, politikanın işe yaraması için ön koşuldur. Politika önce yalnızca raporlama kipinde yayına alınıp gelen ihlaller incelenerek sıkılaştırılabilir.
Kontrol listesi
- Kaçış kaydetme sırasında değil, çıktı üretilirken yapılıyor.
htmlspecialcharsçağrılarında bayraklar ve karakter kümesi açıkça veriliyor.- Öznitelik, adres ve betik bağlamları için ayrı kodlama kullanılıyor.
- Kullanıcı adresleri protokol denetiminden geçiyor.
- Görüntüleme kodu iş mantığından ayrı dosyalarda.
- Zengin metin izin listesiyle temizleniyor, düzenli ifade kullanılmıyor.
- İçerik güvenlik politikası ve ilgili güvenlik başlıkları tanımlı.
- JSON ve düz metin çıktılarda HTML kaçışı uygulanmıyor, kaçış biçime göre yapılıyor.
İlgili yazılar
PHP eğitim serisi
Bu yazı, temel sözdiziminden üretim ortamına kadar ilerleyen 25 bölümlük serinin bir parçası. Bölümler sırayla okunacak biçimde hazırlandı, ancak her biri tek başına da kullanılabilir.
Temeller
- Kurulum ve temel sözdizimi
- Döngüler ve akış kontrolü
- Diziler
- Fonksiyonlar
- String fonksiyonları
- Süper globaller
- Form işleme
- Dosya işlemleri
Dilin ayrıntıları
Nesne yönelimli PHP
- Sınıf ve nesne
- Kalıtım, arayüz ve trait
- Statik üyeler, sabitler ve enum
- Namespace, Composer ve autoload
Veri ve güvenlik
- PDO ile veritabanı işlemleri
- Oturum yönetimi ve kimlik doğrulama
- Güvenli çıktı ve XSS (bu yazı)
- JSON ile veri değişimi
Dış dünya ve üretim
Yorumlar
İlk yorumu sen yap.
İlgili Yazılar

PHP Uygulamasını Üretime Almak: Yapılandırma ve Güvenlik
Geliştirme makinesinde çalışan kod ile üretimde güvenle çalışan kod arasındaki fark yapılandırmadır. Ayarlar, izinler, dağıtım akışı ve son kontroller.

PHP ile REST API Yazmak: Yönlendirme ve HTTP Durum Kodları
Bir API'nin kalitesi çoğunlukla ayrıntılarda belli olur: doğru durum kodu, tutarlı hata biçimi ve öngörülebilir adresler. Saf PHP ile uçtan uca kurulum.

PHPUnit ile Birim Test Yazmak: Kurulum, Sahte Nesneler ve Kapsam
Testin amacı hata bulmak değil, değişikliği güvenle yapabilmektir. Kurulum, ilk test, sahte nesneler ve test edilebilir kodun özellikleri.