4 Ağustos 2008 Pazartesi

strip_tags() - allowable tags ve güvenlik problemleri

strip_tags() fonksiyonu, izin verilen etiketler olarak bir parametre alıyor. bu parametrede, izin verilen HTML etiketleri kullaniliyor ve isleme sokuldugunda yok edilmiyor.

XSS saldirilarina karsi, bir cok yerde strip_tags() fonksiyonunun onerildigini gorebilirsiniz. Fakat, allowable tags kismina sadece etiketini eklemeniz bile guvenlik acisindan ciddi problemler dogurabiliyor. ( programcilar, zararsiz olarak gordukleri bu etiketleri kullanabiliyorlar bu parametrede.)

Ornegin, BB-CODE'lardan uzak duralim diye strip_tags() icinde A etiketine izin verdik diyelim.

Ilk olarak textarea kismina yazin, sonuc hatasiz cikacak.Evet, filtrelenmis.

İkinci seferde
y gibi bir sey yazin, a etiketi allowed tags icinde oldugu icin duzgun bir sekilde calisacaktir.

Fakat, sorun su ki, yetenekli bir saldirgan icin a etiketi bile yeterli olacaktir.Zira, a etiketi icindeki "onclick" eventi javascript calistirmak icin yeterli.Bu da cookie calma gibi ciddi sorunlara yol acabilir.

Ornegin, yukardaki basit betigi gecen bir saldiri deseni:

Kod:

ya da cookie (cerez) calalim:

Kod:

Bu tip sorunlarla karsilasmamak icin, bb-code teknigini kullanabilirsiniz.

PHP XSS SALDIRI VE KORUNMA

Bu makaleyi okuyan PHP kod yazarları;
- XSS'in ne olduğunu,
- Çerezlerin (cookie) çalışma şeklini,
- XSS'den rahatça nasıl korunabileceklerini öğrenecekler.


XSS Nedir?
XSS (Cross Site Scripting - Çapraz Kod Çalıştırma) kısaca, HTML ve JavaScript yardımıyla bir sitede, siteye giren kullanıcıya tehlike arz edecek şekilde kod çalıştırmaya denir. Ziyaretçilerinizden bilgi almak amaçlı ya da onlara ait hesapları tekrar tekrar şifre girmeden kullanmaları için yollamış olduğunuz çerezleri -cookies- XSS yardımıyla çalabilirler ve kendilerini, sisteme o ziyaretçiymiş gibi gösterebilirler.

Çerezler Nasıl Çalışır?
Bir çerez, istemci -yani siz- tarafında herhangi bir bilgiyi, oturum numarasını vs. saklamaya yarayan ve sadece kendisinin yollandığı alan adı (domain) için bilgilerini veren bir araçtır denilebilir. Yani herhangi bir siteye girdiğinizde, karşı taraftan size bilgilerinizi tutmak için bir çerez yollanabilir. (Eğer tarayıcınızın güvenlik ayarları çok sıkı değilse, otomatik olarak kabul ediecektir bu çerezler :-))

Kullanım alanları, genel olarak kullanıcı bilgileri ya da sizin karşı taraftaki oturum numaranızı karşılaştırmak içindir. Bu noktada, karşı tarafın, sizin diğer çerezlerinizin içindeki bilgileri ele geçirmenizden şüphe edebilirsiniz belki; ancak başta da bahsettiğimiz gibi, tarayıcılar sadece çerezi isteyen yerin alan adı ve dosya yolu (örn. www.foo.com/hede adresine ayarlı bir çerez, asla www.foo.com/hodo adresine yollanmaz) uyuşuyorsa bu bilgileri yollarlar, olağanüstü bir durum dışında bu bir güvenlik sorununa yol açmaz :-)

XSS Nasıl Çalışır?
Bu yazının tehlikeli işler yapmaya meyilli olan kişiler tarafından da okunma ihitmali olduğu için açık açık anlatım yoluna gitmeyi düşünmüyorum :-D Şöyle bir gözden geçirecek olursak;

JavaScript dilinde çerezleri yönetmeyi sağlayan bir cookie yöntemi bulunmaktadır. (yani; document.cookie) Bu yöntem yardımıyla sayfaya ait olan çerezler alınıp, okunabilir ve değiştirilebilir. [1] XSS açıklarından yararlanan kötü niyetli kişiler de, JavaScript'in bu özelliğinden yararlanarak sayfayı gezen kullanıcının çerez bilgilerini alıp, kendilerininkiyle değiştirmeye çalışırlar. Bu nedenle XSS açığı bulunan herhangi bir sayfayı gezen ziyaretçinin, eğer ilgili sayfadan bir hesabı da varsa çok dikkatli olması gerekmektedir; bunun yanında site sahiplerinin de bu konuda ciddi şekilde önlem almaları, olayların patlak vermesini de engeller. Çözüm yollarına birazdan değineceğiz...

[1] PHP'nin yeni sürümleriyle beraber gelen, HTTP başlıklarındaki Set-Cookie özelliğine eklenecek olan bir HttpOnly deyimiyle, tarayıcılar deyimin geçtiği çerezi JavaScript yoluyla almayacak/göstermeyecektir...

Şimdi bu saldırılardan nasıl korunulacağımızı öğrenelim...

Koruma Yolları Nelerdir?

Yukarıda bahsettiğimiz XSS saldırılarından iyi bir şekilde korunabilmek için, öncelikle kullanıcı tarafından gelen bilgileri nasıl filtreleyebileceğimizi düşünelim... Çerez çalmak için gerekli olan kodun çalıştırılması için, öncelikle etiketleri (tags) arasına yazılması gerekir ki, buradan HTML etiklerinde kullanılması zorulu olan <> karakterlerini düzgünce filtrelemenin, sorunumuzun büyük bir kısmı çözebileceğimiz anlamına gelir. O hâlde:

Kod:

 htmlentities($xss_potansiyelli_veri);
// htmlentities() fonksiyonu, kendisine parametre olarak
// konulmuş verinin içindeki <, >, & gibi karakterleri
// '<', '>' gibi pratikte zararsız, HTML sayfada ise
// normal görünecek karakter gruplarıyla değiştirir.
// Daha fazla koruma için ikinci parametre olarak
// 'ENT_QUOTES' diyebilir ve tırnak işaretlerini de
// filtreleyebiliriz...
// (bkz. php.net/htmlentities)
?>

gibi birşeyler yapalım :-) Peki sadece bu tek fonksiyon yeterli olabilir mi? Aslında, hayır... URL adresinden yollanacak olan bilgiler, herhangi bir ASCII karakterinin onaltılık (hexadecimal) formatında da olabileceği için (örn. boşluk için %20 gibi) bu işlemi atlatabilir. İşimizi sağlama almak için, URL'den gelen ASCII formatındaki bilgiler de dahil olmak üzere, gönderilen veriyi normal hâline getiren urldecode() fonksiyonunu kullanabiliriz:

Kod:

 htmlentities(urldecode($xss_potansiyelli_veri));
?>

Peki bu yeterli mi? :-D Bazen, verinin kullanıldığı yere göre, farklı filtrelemeler yapmamız gerekebilir. Bu tür filtrelemeler için quotemeta() [2] ve addslashes() [3] fonksiyonları biçilmiş kaftandır. Tüm bunları göz önüne alarak, aşağıdaki gibi bir fonksiyon yazalım:

[2] . \ + * ? [ ^ ] ( $ ) karakterlerinin önüne \ (backslash) karakteri koyar.
[3] Tırnak işaretinin önüne \ karakteri koyar.

Kod:

 function dataFilter($data, $mod = 1, $op = 0) {
// Öntanımlı fonksiyon çalışma düzeyini 1, seçeneği 0 olarak ayarladık.
$data = urldecode($data);
// Güvenli filtreleme için URL'den gelen veriyi çözdük.

if($mod == 0) {
// Düzey 0 ise; sadece çözülmüş hâlini döndür:
return $data;
} else if($mod == 1) {
// Düzey 1 ise; htmlentities() uygulanmış hâlini döndür,
// Seçenek 1 ise; fonksiyonun tırnakları da filtrelemesini sağla:
return ($op == 0) ? htmlentities($data) : htmlentities($data, ENT_QUOTES);
} else if($data == 2) {
// Düzey 2 ise; sadece quotemeta() uygulanmış hâlini döndür,
// Seçenek 1 ise; htmlentities()'i de ekle,
// Seçenek 2 ise; tırnakları da filtrele:
return ($op == 0) ? quotemeta($data) :
(($op == 2) ? htmlentities(quotemeta($data), ENT_QUOTES) : htmlentities(quotemeta($data)));
} else if($mod == 3) {
// Düzey 3 ise; addslashes() uygulanmış hâlini döndür,
// Seçenek 1 ise; addslashes()\'a quotemeta() fonksiyonunu da ekle,
// Seçenek 2 ise; seçeneksiz duruma htmlentities() fonksiyonunu ekle,
// Seçenek 3 ise; 1. seçeneğe htmlentities() fonksiyonunu ekle:
if($op == 0)
return addslashes($data);
else if($op == 1)
return addslashes(quotemeta($data));
else if($op == 2)
return htmlentities(addslashes($data));
else if($op == 3)
return htmlentities(addslashes(quotemeta($data)));
}
}
?>

(Bu fonksiyonda bahsettiğimiz bilgilere bir de çalışma düzeyi ekleyerek hepsini bir arada kullanmayı amaçladım, daha farklı alanlarda tek fonksiyonla idare edilebilsin diye :-P)

Yukarıdaki fonksiyonu daha genel bir şekilde yazının devamında kullanacağız, ancak ondan önce kullanımı hakkında kısa birkaç örnek;

- dataFilter($veri, 1): $veri değişkenine htmlentities() uygulanır. Ortalama bir filtreleme.
- dataFilter($veri, 2, 2): Tırnakların da htmlentities() ile filtrelendiği, quotemeta()'nın kullanıldığı bir düzey seçeneği... Çok fazla karakter filtrelediği için her veride kullanmanız sorun çıkarabilir.
- dataFilter($veri, 3, 3): Bahsettiğimiz tüm fonksiyonların kullanıldığı paranoyakça bir filtreleme :-D Çok fazla gerek duyucağınızı sanmıyorum, genelde sorun çıkarır hepsi bir arada olunca :-)

Kullanıcıdan Alınan $_POST ve $_GET Dizileri
Yazımızın bu son kısımında, hem yukarıda yazdığımız işe yarar ama çok karışık -kim ezberler ki zaten? big_smile- fonksiyonumuzu kullanabileceğimiz, hem de kullanıcıdan gelen iki önemli diziyi nasıl filtreleyeceğimizi göstereceğiz...

O hâlde;
- Bu iki sabit diziyi dataFilter() fonksiyonumuzun htmlentities() ve addslashes() fonksiyonlarının kullanıldığı çalışma düzeyinde filtreleyelim ve
- $_POSTS, $_GETS isimlerinde filtrelenmiş veriyi saklayan bir dizi oluşturalım:

Kod:

 $_GETS = array();
$_POSTS = array();

foreach($_GET as $key => $value) {
$_GETS[$key] = dataFilter($value, 3, 2);
}

foreach($_POST as $key => $value) {
$_POSTS[$key] = dataFilter($value, 3, 2);
}
?>
Yazdığım ilk uzun makale olduğu için bazı kısımları anlaşılmaz -heyecandan tabi tongue- olabilir, sorularınızı yorumlar yardımıyla iletebilirsiniz...