PHP ve Form Güvenliği

Kategori: (PHP - Hypertext Preprocessor) Yazan: admin, 20-12-2007

Herkese merhaba. Öncelikle belirtmek isterim ki eğer yeni bir PHP programcısı iseniz bu yazının size çok fazla yararı dokunmayacaktır. Bu nedenle öncelikle diğer PHP güvenlik yazılarını okumanızı ve daha sonra bu yazıyı ele almanızı tavsiye ederim.

1995 yılı Haziran ayının 8′inde comp.infosystems.www.authoring.cgi USENET grubunda bir anons yapıldı. Personel Home Page Tools (PHP) baÅŸlığını taşıyan bu e-postanın yazarı Rasmus LERDORF’tu ve PHP’nin 1.0 sürümünü duyuruyordu. O zamandan bu zamana 10 yıl geçti ve PHP dünyanın en popüler web yazılım araçlarından biri oldu. Geçen süre içerisinde PHP’nin yaygınlaÅŸması ve geliÅŸtirilmesi doÄŸru orantılı olarak artarken PHP ile oluÅŸturulmuÅŸ İYS’ler (İçerik Yönetim Sistemleri) ortaya çıkmaya baÅŸladı iÅŸte tam bu sırada birileri (kötü çocuklar) GPL/özgür lisanlı bu yazılımların içerisindeki hatalardan faydanan yazılımlar yapmaya baÅŸladılar (”spam” içerik yapan yazılımlar gibi).

Artık pek çok kiÅŸi kolay ve yaygın olmasından dolayı (sokağınızdaki bakkal gibi) hazır İYS kullanırken yapılan saldırılardan da başını kaldıramaz olmuÅŸtu. Pek çok dökümanda web yazılımların kullanıcıdan veri alma yöntemlerinde ($_POST, $_GET, $_COOKIE,$_FILES) filtreleme yapması gerektiÄŸi yazar ve bununla ilgili yöntemlerden örnekler verilir. Ancak bu saldırganları durdurmaya asla yeterli deÄŸildir. Zira yazılımdaki kurallara uygun ancak “spam” içerikli yazılar (ya da “brute force” ile ÅŸifre bulmaya çalışmak için) İYS’lere post edilerek gereksiz veri tabanı ve sunucu yorgunluÄŸuna sebeb olurlar. Peki bunun için neler yapabiliriz? Dilerseniz ikinci bölüme gidelim ve bu sorunun analizini orada yapalım.

2. Sorun Analizi

Sorun: Bir ya da daha fazla istemciden gelen çok miktarda veri (örn: brute force)

Analiz: Hiç kimse dakikada 45 defa aynı metni yollayabilecek kadar hızlı deÄŸildir. Demek ki bunu yapan saldırgan bu iÅŸ için bir yazılım kullanıyor (yaramaz çocuk) Oysa ben sadece “post”tan gelen verileri alıp kontrol edip oturumumu baÅŸlatıyordum (tabii gelen bilgiler doÄŸru ise); bir bakalım bakalım:


<!-- html -->
<form action="$Fqdn" method="post">
Username: <input type="text"  size="15" name="uname" /> 

Password : <input type="password"  size="15" name="passwd" /> 

<input type="submit" name="button" value="submit" / >
</form>

<!-- php -->
<?php
    if(ctype_alnum($_POST['uname'])){
	$uname=$_POST['uname];
    }

    if(ctype_alnum($_POST['passwd'])){
	$passwd=$_POST['passwd'];
    }

    if(!$passwd || !$uname) {
	return CreateErrorPage(’Invalid User Name Or Password’);
    }

    $conn=DB::Connect(’mysql://timu:mypass@localhost/dbname’);
    /* uzatmayalım burada select ile verileri kontrol ediyoruz …*/
    if($conn->>RecordCount() != 0) {
	/* giriş başarılı burada da oturum yönetimi başlatıyor ve kullanıcıyı içeri alıyoruz */
    }
    else {
        /* giriş başarısız demekki yanlış şifre kullanıcıyı geri yolluyoruz..*/
    }
?>

Saldırgan, elindeki yazılım ile benim form alanımdaki “name” alanındaki isimlere deÄŸer vererek “post” ile yolluyor olmalı.

Çözüm: Bu kısmıda diğer bölümde ele alalım:

3. Çözüm

Saldırgan form alanındaki name deÄŸiÅŸkenlerine deÄŸer verip yolluyor ise benim buna çözümüm basitçe ÅŸu olacaktır: Form alanları için asla sabit olmayan “name” deÄŸeri.

Biraz daha açmak gerekirse:

Password : <input type="password" size="15" name="passwd" />

Satırındaki name=”passwd” özelliÄŸinin passwd kısmının rastsal (random) ÅŸekilde deÄŸiÅŸtiÄŸini varsayalım. yani kullanıcının her baÅŸarısız denemesinde:

birinci deneme: <input type="password" size="15" name="asdasd" />
ikinci deneme: <input type="password" size="15" name="pdfgn" />
üçüncü deneme: <input type="password" size="15" name="qytre" />

ÅŸeklinde “name” özelliÄŸinin (ya da özniteliÄŸinin artık neyse) sürekli deÄŸiÅŸtirildiÄŸini düşünün. Evet evet ama “nasıl yani” dediÄŸinizi duyar gibiyim.

Cevabını hemen vereyim. Oturum yönetimi. Evet dikkatli tasarlanmış bir oturum yöntemi ile bunu yapabilirsiniz ancak bu oldukça dikkat isteyen bir iştir ve bitmiş projelere uygulanması en az yeniden yazılması kadar zordur. Bu nedenle biz yeni yazılacak bir sistem için düşünelim. Az öncede belirttiğim gibi bu işlem biraz meşakkatli, o nedenle bu işlemi yapabilmek için gereklilik listemizi oluşturmak ve daha sonra mantığımızı geliştirmek, en son olarak da kodlamaya girişmeliyiz. Bunların her birini ayrı başlıklar altında işleyeceğiz..

4. Gerekliliklerin tespiti

1) SaÄŸlam tasarlanmış bir oturum sistemi. KiÅŸisel olarak kendi geliÅŸtirdiÄŸim “cookie” tabanlı ve SQL destekli bir oturum sistemi üzerinde çalışıyorum ancak bu kodu buraya koymak amaç dışına çıkmamıza neden olacağından basitçe ve kabaca methodları anlatıp geçeceÄŸim..

2) Oturum yönetimi ve diğer verileri saklayacağımız bir SQL sunucusu tercihen MySQL yada PostgreSQL.

3) Veri tabanı erişim katmanı için bir sınıf (benim kişisel tercihim adodb üzerine ancak siz kendiniz yazabilir yada diğer sınıflardan birini kullanabilirsiniz) aslında bu bir gereklilik değil ancak işlerimizi oldukça kolaylaştıracak. Bekleyin göreceksiniz.

Şimdilik bu kadar artık kodlamaya başlayalım bu arada da düşünelim. (Bu çok iyi bir yol değildir her zaman önce düşünün daha sonra kodlamaya geçin.)

5. Kodlama

Daha öncede belirttiğim gibi adodb sınıfını veri tabanı erişimi için kullanıyorum ve örneklerde bu sınıf üzerinde yer alan metodları kullanarak vereceğim ancak pek çok sınıftada methodlar benzerlik gösterir. Elbette tüm sınıfları ve methodları anlatacak değilim sadece ana mantığı kuracağız. Öncelikle Oturum yönetimimizi bir ele alalım.

Kullanıcı sisteme ilk geldiğinde eğer daha önce açmı olduğu bir oturum yoksa hemen yenisi açılmalı. Yoksa randomize name alanlarımızı takip edemeyiz.


<?php
    /* Session Sınıfı Örneği */
    include 'adodb.inc.php';
    include 'drivers/adodb-mysql.inc.php';
    Class Session Extends adodb_mysql {

	var $_sessionvars;

	Function Session() {
	    return true;
	}

	Function __construct(){     // PHP5 support
	    return $this->Session();
	}

        /* Veri tabanı bağlantısı cookie controlu ayrıntılı olarak anlatılmayacaktır ...
           sadece çokça kullanacağımız 5 method ve 1 özelliği tanıtacağım..
	*/

	function SetSessionVars($name, $value) {
	    $this->_sessionvars[$name] = $value;
	    $this->_WriteSesion();
	}

	function GetSessionVars($name) {
	    return $this->_sessionvars[$name];
	}

	function _WriteSession() {
	    $vars=serialize($this->_sessionvars);
	    $SQL=”update session set sess_vars=’$vars’ where ip_addr=’$this->_ip_addr’ and sess_id=’$this->_sessionid’ lastaction=now()”,
	    $this->execute($SQL);
	}

	function _readSession(){
	    $SQL=”select sess_vars from session where ip_addr=’$this->_ip_addr’ and sess_id=$this->_sessionid”;
	    $rs = $this->execute($SQL);
	    list($vars) = $rs->fields;
	    $vars=unserialize($vars);

	    foreach($vars $k=>$v) {
		$this->_sessionvars[$k] = $v;
	    }
	}
    }

?>

Åžimdi session sınıfımızı biraz anlatalım: Session sınıfı adodb_mysql sınıfından türetilir yani ondan miras alır. Aslında bu kısımdan sonrası biraz karışık zira $_COOKIE deÄŸiÅŸkeninden sessionid’yi almak $_SERVER’dan ise istemcinin IP adresini bulmak gibi iÅŸlemler var, ya da oturum açılmamışsa yeni oturum açmak vs. ancak bunları anlatmayacağız.

Session::_readSession();

Bu metod “private” bir metoddur ve veri tabanından kullanıcının oturum deÄŸiÅŸkenlierini alır ve $this->_sessionvars‘a kaydeder.

Session::_WriteSession();

bu metod “private” bir metoddur ve $this->_sessionvars‘ı sıralı hale getirip (serialize edip) veri tabanına yazar.

Session::GetSessionVars();

Bu metod “public” bir methodtur ve $this->_sessionvars dizisi içerisinden istenen deÄŸiÅŸkeni geri yollar.

Session::SetSessionVars();

Bu method “public” bir metoddur ve $this->_sessionvars dizini içerisine kendisine parametre olarak verilmiÅŸ isim ve deÄŸerleri kaydeder, daha sonra veri tabanına yazmak için Session::_WriteSesion() fonksiyonunu çağırır.

Şimdi bir random değer üreten bir function yazalım:


    function randomvalue() {
	$keys='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
	$randnum = random(5, 15);
	$len=strlen($keys);
	for($i=1; $i < $randnum; $i++) {
	    $each= random(1 ,$len)
            $randomval .= $keys[$each];
	}

	return $randomval;
    }

Tamam artık her şeyimiz hazır gibi.

Åžimdi de kullanmaya baÅŸlayalım bakalım, ilk önce formumuza bir isim verelim ki daha sonra diÄŸer form alanlarımız ile karışmasın, evet, ismi Login olsun ve input alanlarımıza da isim verelim. Madem sabit isimlerimiz olmayacak o halde neden uÄŸraşıtırıyorsun bizi dediÄŸinizi duyar gibiyim. Birazdan göreceksiniz ki bu isimleri asla ziyaretçimiz görmeyecek, bu isimleri session’a kaydetmek için kullanacağız ki daha sonra kullanıcıdan verilerimizi alabilelim. Örn:


<?php
    /* burada diğer rutinler sınıfları oluşturulması
    veri tabanı bağlantısının yapılması vs. gibi işlemler var*/

    $nameattiribute = randomvalue();
    $session->SetSessionVars('LoginUserName', $nameattiribute);
    $passwdattiribute = randomvalue();
    $session->SetSessionVars('LoginPassword', $passwdattiribute);
?>

<form action="<?=$Fqdn?>" method="post">
Username: <input type="text"  size="15" name="<?=$nameattiribute?>" />

Password : <input type="password"  size="15" name="<?=$passwdattiribute?>" />

<input type="submit" name="button" value="submit" />
</form>

Şimdi de form bilgilerinin Post edildiği dosyamıza bakalım:



<?php

    /* burada diğer rutinler sınıfları oluşturulması
    veri tabanı bağlantısının yapılması vs. gibi işlemler var*/

    $nameattiribute = $session->GetSessionVars('LoginUserName');
    $passwdattiribute = $session->GetSessionVars('LoginPassword');

    if(ctype_alnum(htmlspecialchars($_POST[$nameattiribute]))) {
	$uname=$_POST[$nameattiribute];
    }

    if(ctype_alnum(htmlspecialchars($_POST[$passwdattiribute]))) {
	$passwd=$_POST[$passwdattiribute];
    }

    if(!$passwd || !$uname) {
	return CreateErrorPage(’Invalid User Name Or Password’);
    }

    /* Burada SQL Sunucumuzdan verilerin doÄŸruluÄŸunu kontrol ediyoruz.. */
    if($rs->RecordCount() != 0) {
	/* giriş başarılı*/
    }
    else {
    /* giriş başarısız tekrar login formuna yönlendirme yapmalıyız.. */
    }
?>

Evet sanırım anladınız değil mi? :)

Åžimdi biraz açalım. $nameattiribute = randomvalue(); ile bir random deÄŸer alıp bu deÄŸeri ‘LoginUserName’ ismi ise oturumumuza ekliyoruz. Aynı iÅŸlemi password içinde yapıyoruz elbette. Daha sonra formumuzun post edildiÄŸi sayfamızda $session->GetSessionVars('LoginUserName'); ile oturumumuzdaki deÄŸeri alıyoruz ve bu isimde bir $_POST içerisinde bir “key” var mı kontrol ediyoruz; elbette ki htmlspecialchars ve ctype_alnum ile kontrol etmeyi de unutmuyoruz.

Gerisi size kalmış, artık saldırganlar sizin HTML kodlarınızı okuyarak “spam” ya da “bruteforce” yapabilecek uygulamalar yazamayacaklar çünkü her sayfa isteminde input alanlarındaki “name” özniteliÄŸi sürekli deÄŸiÅŸecek.

5. Lisans

Belge GNU/FDL lisansı ile lisanslanmıştır.

Belge içerisindeki tüm fikir hakları kamuya aittir.

Notlar

Eğer döküman hakkında fikirleriniz varsa yada bir şeyler eklemek isterseniz selamtux at gmail nokta com adresi üzerinden yada irc.freenode.net sunucularındaki #fazlamesai, #debian.tr, #pardus-devel (hayir pardus geliştiricisi değilim) kanalları üzerinden iletişime geçebilirsiniz.

Timu EREN
21 Haziran 2005

Makalenin tartışma adresi: http://fazlamesai.net/modules.php?name=News&file=article&sid=3028

Yorum Yazın

Gues World - Photoshop
Netkabus - Bilgi Portalý
reklam alaný
reklam alaný