1. Giriş
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=¤¤¤¤¤¤ize($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=un¤¤¤¤¤¤ize($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 (¤¤¤¤¤¤ize 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='ABCDEFGHIJKLMNOPQRSTUVW XYZabcdefghijklmnopq rstuvwxyz0123456789';
$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(htmlspecialchar s($_POST[$nameattiribute]))) {
$uname=$_POST[$nameattiribute];
}

if(ctype_alnum(htmlspecialchar s($_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.