PHP'de XSS ve Güvenli Çıktı Üretimi

PHP'de XSS ve Güvenli Çıktı Üretimi

5 dakika okuma

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.

PHP10 satı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.

PHP11 satır
// ö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.

PHP5 satı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.

HTML5 satır
<!-- gorunum/yazi.php -->
<article>
    <h1><?= e($yazi['baslik']) ?></h1>
    <p><?= e($yazi['ozet']) ?></p>
</article>
PHP10 satır
// ş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.

PHP4 satır
// 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.

PHP7 satır
// 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.

PHP5 satır
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

  1. Kurulum ve temel sözdizimi
  2. Döngüler ve akış kontrolü
  3. Diziler
  4. Fonksiyonlar
  5. String fonksiyonları
  6. Süper globaller
  7. Form işleme
  8. Dosya işlemleri

Dilin ayrıntıları

  1. Tip sistemi ve strict_types
  2. Hata ve istisna yönetimi
  3. Düzenli ifadeler
  4. Tarih ve saat işlemleri

Nesne yönelimli PHP

  1. Sınıf ve nesne
  2. Kalıtım, arayüz ve trait
  3. Statik üyeler, sabitler ve enum
  4. Namespace, Composer ve autoload

Veri ve güvenlik

  1. PDO ile veritabanı işlemleri
  2. Oturum yönetimi ve kimlik doğrulama
  3. Güvenli çıktı ve XSS (bu yazı)
  4. JSON ile veri değişimi

Dış dünya ve üretim

  1. cURL ile dış API çağırma
  2. Kendi REST API'ni yazmak
  3. PHPUnit ile birim test
  4. Hata ayıklama ve performans ölçümü
  5. Üretime alma ve güvenlik

Yorumlar

Bu form reCAPTCHA ile korunur; Google Gizlilik Politikası ve Hizmet Şartları geçerlidir.

İlk yorumu sen yap.

İlgili Yazılar