İçeriğe geç
academia.sh

Ders 12 / 15

Siteler Arası Betik Çalıştırma Önleme

Kullanıcıdan gelen metni sayfaya basarken kaçışlamanın sonucu nasıl daralttığını, bağlama göre kaçışlamayı ve çift kaçışlamayı ölçmek; kaçışlama sahibi değiştirmez.

Yayın Güncelleme

Bu derste

  • Kullanıcı metnini htmlspecialchars() ile çıktı anında kaçışlar ve ham çıktıyla yapısal farkı ölçer
  • Metin ve öznitelik bağlamında kaçışlamanın aynı fonksiyonla yapıldığını, bağlamı açıkça adlandırır
  • Çift kaçışlamayı ölçer ve kaçışlamanın tek yerde, çıktı anında yapılmasını gerekçelendirir
İçindekiler

SQL Enjeksiyonu Önleme dersinde değer sorgu metninden ayrıldı ve sorgunun sahibi sunucu oldu. Değerin gittiği başka bir yer sayfadır: kullanıcı bir ad girer, uygulama bu adı HTML’e basar. Siteler arası betik çalıştırma (cross-site scripting, XSS) önleme sorusu şudur: basılan metnin yapısını, yani hangi parçasının işaretleme sayılacağını kim belirliyor?

GÜ3. Bu derste kaçışlanan metinler dört zararsız örnektir: Tom & Jerry, <b>kalın</b>, "tırnak" ve O'Brien. Amaç, işaretleme karakterlerinin metin olarak çıkıp çıkmadığını ölçmektir; çalışan bir betik üretmek ya da başka bir siteden istek yapmak ölçülmez. Koşan kod savunma tarafındadır.

Kaçışlama: Metni Metin Olarak Basmak

Kaçışlama (escaping), işaretleme dilinde özel anlam taşıyan karakterleri, aynı karakteri metin olarak gösteren karşılıklarla değiştirir. PHP’de bu işi htmlspecialchars() yapar: &, <, >, çift tırnak ve tek tırnak için &amp;, &lt;, &gt;, &quot; ve &#039; yazar. Aşağıdaki dosya iki fonksiyon tanımlar: ham çıktı üreten ve kaçışlayan. Ham çıktıyı üreten fonksiyon yalnız karşıtlık için vardır.

<?php
// cikti.php — ham ve kaçışlanmış çıktı
declare(strict_types=1);

function kacisla(string $metin): string {
    return htmlspecialchars($metin, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function selamlamaHam(string $ad): string {
    return '<p>Merhaba ' . $ad . '</p>';
}

function selamlama(string $ad): string {
    return '<p>Merhaba ' . kacisla($ad) . '</p>';
}

function girdiAlani(string $ad): string {
    return '<input value="' . kacisla($ad) . '">';
}

Dört örnek metni önce kacisla()’dan, sonra iki selamlama fonksiyonundan geçirelim. Ham çıktıda <b>kalın</b> sayfanın yapısına karışır; kaçışlanan çıktıda < karakteri sayısı sabit kalır.

<?php
// kacisla.php — kaçışlanan karakterler
declare(strict_types=1);
require 'cikti.php';

foreach (['Tom & Jerry', '<b>kalın</b>', '"tırnak"', "O'Brien"] as $metin) {
    echo $metin, ' → ', kacisla($metin), "\n";
}
echo "\n";
foreach (['ayse', '<b>kalın</b>'] as $ad) {
    echo mb_str_pad($ad, 14), ' ham ', substr_count(selamlamaHam($ad), '<'),
        ' · kaçışlı ', substr_count(selamlama($ad), '<'), " etiket başlangıcı\n";
}
Tom & Jerry → Tom &amp; Jerry
<b>kalın</b> → &lt;b&gt;kalın&lt;/b&gt;
"tırnak" → &quot;tırnak&quot;
O'Brien → O&#039;Brien

ayse           ham 2 · kaçışlı 2 etiket başlangıcı
<b>kalın</b>   ham 4 · kaçışlı 2 etiket başlangıcı

Dört metin de metin olarak çıktı: &, <, >, çift ve tek tırnak karşılıklarına döndü. Sayım yapıyı gösterir. ayse adında ham ve kaçışlı çıktı aynıdır, iki etiket başlangıcı vardır (<p> ve </p>). <b>kalın</b> adında ham çıktı dört etiket başlangıcına çıkar; kaçışlı çıktı ikide kalır. Ham çıktıda istemcinin değeri sayfanın yapısına iki etiket ekledi, kaçışlıda ekleyemedi.

Bağlama Göre Kaçışlama

Aynı fonksiyon her bağlamda yeterli değildir; yeterli olduğu bağlam, çıktının nereye gittiğiyle belirlenir. Metin düğümünde < ve & yeterlidir. Çift tırnaklı bir öznitelik değerinde tırnağın da kaçışlanması gerekir, çünkü değer tırnakla biter. ENT_QUOTES bayrağı ikisini de kapsar. girdiAlani() bu bağlamı kullanır.

<?php
// baglam.php — öznitelik içi kaçışlama
declare(strict_types=1);
require 'cikti.php';

echo girdiAlani('"tırnak"'), "\n";
echo girdiAlani("O'Brien"), "\n";
echo selamlama("O'Brien"), "\n";
<input value="&quot;tırnak&quot;">
<input value="O&#039;Brien">
<p>Merhaba O&#039;Brien</p>

Öznitelik bağlamında çift tırnak &quot; olarak çıktı, yani değeri erken bitiremez. Tek tırnak her iki bağlamda &#039; olur. Sonuç şudur: bağlam adlandırılmadan “kaçışladım” denmez. Metin düğümü için yeterli olan, tırnaksız bir özniteliğe ya da başka bir bağlama (betik içeriği, adres) aynen taşınmaz; o bağlamlar için ayrı kaçışlama gerekir ve bu ders yalnızca metin düğümü ile çift tırnaklı özniteliği kapsar.

Taraf: Kaçışlama Sahibi Değiştirmez

Aynı sorunun taraf ölçümü, taraf.php çekirdeğiyle yapılır. Karar sırası istemci, üçüncü taraf, ortam; hiçbiri değiştiremediyse sunucu.

<?php
// taraf.php — bu sonucu kim değiştirebiliyor?
declare(strict_types=1);

// Kodun dışındaki taraflar, karar sırasıyla: her istek, her dış çağrı, her dağıtım.
const DIS_TARAFLAR = ['istemci', 'üçüncü taraf', 'ortam'];

/** Sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla ayrı ayrı hesaplar. */
function sina(callable $sonuc, array $sapmalar): array {
    $taban = $sonuc([]);
    $degisti = [];
    foreach (DIS_TARAFLAR as $taraf) {
        if (array_key_exists($taraf, $sapmalar)) {
            $degisti[$taraf] = $sonuc([$taraf => $sapmalar[$taraf]]) !== $taban;
        }
    }
    return $degisti;
}

/** Sonucu değiştirebilen ilk dış taraf; hiçbiri değiştiremiyorsa sunucu. */
function taraf(array $degisti): string {
    foreach (DIS_TARAFLAR as $t) {
        if ($degisti[$t] ?? false) return $t;
    }
    return 'sunucu';
}

function tarafRaporu(string $ad, callable $sonuc, array $sapmalar): string {
    $degisti = sina($sonuc, $sapmalar);
    $satirlar = [];
    foreach ($degisti as $t => $d) $satirlar[] = $t . ($d ? ' değiştirdi' : ' değiştiremedi');
    $sonTaraf = taraf($degisti);
    echo "$ad → $sonTaraf\n    ", implode(' · ', $satirlar), "\n";
    return $sonTaraf;
}

Sonuç, kaçışlanmış ve kaçışlanmamış selamlama HTML’idir. İstemcinin sapması farklı bir addır: <b>kalın</b>.

<?php
// xss.php — basılan HTML'in tarafı
declare(strict_types=1);
require 'taraf.php';
require 'cikti.php';

tarafRaporu('ham çıktı', fn(array $s) => selamlamaHam($s['istemci'] ?? 'ayse'), ['istemci' => '<b>kalın</b>']);
tarafRaporu('kaçışlı çıktı', fn(array $s) => selamlama($s['istemci'] ?? 'ayse'), ['istemci' => '<b>kalın</b>']);
ham çıktı → istemci
    istemci değiştirdi
kaçışlı çıktı → istemci
    istemci değiştirdi

İki çıktının sahibi de istemcidir. Kaçışlama bu sahibi değiştirmedi, çünkü basılan metin hâlâ istemcinin gönderdiği ada bağlıdır: ad değişince HTML değişir. Değişen, bu değişimin türüdür. Ham çıktıda istemci sayfanın yapısını da değiştirebiliyordu, kaçışlıda yalnız bir metin düğümünün içeriğini değiştirebilir. Kaçışlama sonucu sunucuya taşımaz, istemcinin elindeki alanı daraltır; sahip aynı kalır.

Sınırlayıcı Ölçüm

Kaçışlama tek yerde yapılmalıdır. Bir değer depolanırken kaçışlanır, basılırken yine kaçışlanırsa karakterler iki kez dönüştürülür: & önce &amp; olur, sonra o & yeniden &amp; olur. Ölçüm, aynı metni bir kez ve iki kez kaçışlar.

<?php
// cift.php — çift kaçışlama
declare(strict_types=1);
require 'cikti.php';

$metin = 'Tom & Jerry';
$saklanan = $metin;
echo 'bir kez   ', kacisla($saklanan), "\n";
echo 'iki kez   ', kacisla(kacisla($saklanan)), "\n";
bir kez   Tom &amp; Jerry
iki kez   Tom &amp;amp; Jerry

İkinci kaçışlama, ilkinin yazdığı &amp; içindeki & karakterini yeniden kaçışladı ve tarayıcıda Tom &amp; Jerry metni görünür oldu. Çift kaçışlama güvenlik açığı değildir, görünür bir hatadır; ama onu “düzeltmek” için kaçışlamayı kapatmak ya da bir yere atlamak ham çıktıyı geri getirir. Doğru düzeltme kaçışlamayı tek yere koymaktır: değer depoda ham durur ve basıldığı anda, basıldığı bağlama göre bir kez kaçışlanır.

Özet

  • Bu dersin ölçü sınıfı istemci: kaçışlanmış selamlamada da basılan HTML’i, istemcinin gönderdiği ad değiştirdi.
  • Kaçışlama sahibi değiştirmez, daraltır: <b>kalın</b> ham çıktıda etiket sayısını artırdı, kaçışlı çıktıda artırmadı; sayfanın yapısı istemcinin değeriyle genişlemedi.
  • Aynı fonksiyon metin düğümünde ve çift tırnaklı özniteliğte yeterlidir, ENT_QUOTES ile tek ve çift tırnak da kaçışlanır; bağlam açıkça seçilir.
  • Çift kaçışlama metni bozar: Tom &amp;amp; Jerry. Kaçışlama yalnız çıktı anında, tek yerde yapılır; depoda ham değer durur.

Sonraki Adım

Kaçışlama sayfaya basılan metni sınırladı. Sayfadan gelen bir istek ise başka bir siteden de gelebilir: isteği gerçekten kullanıcının mı yaptığını kim belirliyor? İstek Sahteciliği Koruması dersi bunu bir belirteçle ölçer.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat