php رفع الملفات

حماية ثغرات رفع الملفات

آخر تحيين: 06-06-2014

تخزين بيانات الملف في القاعدة ثغرات رفع الملفات التعريف و الإستغلال


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

حماية إسم و محتوى الملف

  • أول شيء ، لا تتيحوا رفع الملفات إلا للأشخاص الموثوق بهم . أو على الأقل ، يجب على الزائر ، أن يكون مسجلا و عضويته مفعّلة على موقعكم ، و لا يسمح له برفع الملف إلا إذا كان مُتّصلا .
  • لا تسمحوا أبدا برفع ملف ، دون التأكد من امتداده بواسطة اللائحة البيضاء . و لا تستعملوا نهائيا اللائحة السوداء
    <?php 
    $error = '';
    $allowed_extensions = array("gif", "jpg","jpeg", "png");
    $temp = explode(".", $_FILES["milaf"]["name"]);
    $file_extension = end($temp);
    
    if (!in_array($file_extension, $allowed_extensions)) 
    {
      $error .= 'صيغة الملف غير مسموح بها ';
    }
    

    يمكننا إضافة التحقق من صيغة الملف .
  • لا يجب أن يكون إسم الملف أو امتداده فارغا . لهذا ، استعملوا التعابير العادية . يمكنكم أيضا فرض رمز النقطة "." مرة واحدة فقط . حتى لا نسمح بإسم يضم أكثر من امتداد ، مثلا "shell.php.jpg" . في نفس الوقت سننضف النص حتى لا نسمح سوى للحروف و الأرقام اللاتينية ، يجب أيضا عدم السماح بأكثر من 255 حرفا في إسم الملف :
    if(!preg_match('#^[a-zA-Z0-9_-]{1,200}\.(jpe?g|gif|png)$#', $_FILES['milaf']['name']) 
    {
      $error .= 'إسم الملف غير صحيح \n';
    }
    
  • تحديد الحجم الأقصى للملف الذي سيتم رفعه ، حتى لا يتم إثقال و تعطيل الخادوم "denial service" برفع ملفات كبيرة الحجم من طرف المُهاجم ، في المثال أسفله حددت الحجم ب"20 كيلوبايت أي "20000 بايت" . بما أنه يمكن تعطيل الخادوم أيضا بتحميل ملفات كثيرة صغيرة الحجم ، يجب أيضا تحديد الحد الأدنى
    if($_FILES["milaf"]["size"] > 20000 OR $_FILES["milaf"]["size"] < 1000)
    {
      $error .= 'حجم الملف غير المسموح به';
    }
    
  • استعمال getimagesize()، للتحقق من صحة أبعاد الصّورة . في المثال أسفله ، سأفرض قيودا على الحد الأدنى لطول و عرض الصورة . إفعلوا نفس الشيء أيضا للحد الأقصى
    $minwidth = 16;
    $minheight = 16;
    $image_sizes = getimagesize($_FILES['milaf']['tmp_name']);
    if ($image_sizes[0] < $minwidth OR $image_sizes[1] < $minheight)
    {
        $error .= 'لا يجب أن تقل أبعاد الصّورة عن "16*16"';
    }
  • إذا صادف أن كان لديكم ملفان بنفس الإسم . لا تقوموا باستبدال الملف القديم بالجديد . و إلاّ أتحتم للمهاجم استبدال ملفكم البريء بملفه الملغوم . أو أضعف الخسائر ، إتلاف بياناتكم الأصلية بغير قصد .
    if (file_exists("uploads/" . $_FILES["milaf"]["name"]))
    {
       $error .=  "المرجو تغيير إسم الملف ، يوجد ملف يحمل نفس الإسم  ";
    }
  • تجنبوا بصفة قطعية إقحام الملفات المرفوعة باستعمال (include, require, ...) كذلك عدم استعمال الدالة eval . لأن هذه الأخيرة تقوم بتنفيذ أي سكريبت php تصادفه ، بطريقة عشوائية .
  • دائما ، غيرّوا إسم الملف ، قبل تخزينه بصفة نهائية . حتّى تُصعّبوا من إمكانية المهاجم ، إيجاد ملفه . غيروا أيضا امتداده إذا أمكنكم ذلك .
    لتغيير إسم الملف . يمكننا مثلا استعمال الوقت الحالي "time()" ، لإعطاء إسم حصري للملف الجديد ، حتى تتجنبوا احتمال وجود ملفين بنفس الإسم مثال :
    <?php
    //...
      $filename = time().".".$file_extension;
      $destination = "uploads/".$filename;
    
      move_uploaded_file($_FILES["milaf"]["tmp_name"], $destination);
    //...
    ?>

    هذا مثال فقط . يمكنكم الإعتماد على مجموعة من الدوال الخاصة بهذا الشأن ، كما يمكنكم إستعمال أكثر من دالة واحدة لتسمية الملف بإسم حصري . بعض هذه الدوال : "filename() ,uniqid(), MD5(), sha1(), md5_file() , sha1_file()" ...
    عندما تقومون بتغيير إسم الملف باستعمال هذه الطرق . سنحتاج إلى تخزين الإسم الجديد في مكان آخر ، حتى يسهل علينا التعامل معه . هذا هو محور درسنا القادم . سنتعرف على طريقة تخزين معطيات الملف في قاعدة البيانات . لن نُخزن طبعا الملف بأكمله حتى لا نثقل القاعدة . بل سنخزن فقط الإسم الجديد للملف و امتداده مع بعض البيانات الثانوية كحجمه و أبعاده ...
    في هذه الحالة ، للتحقق من عدم وجود ملفين يحملان نفس الإسم باستعمال file_exists . لن نعتمد على إسم الملف الذي تم رفعه . بل على الإسم الجديد

حماية مكان تخزين الملف :

  • أحسن حماية يمكنكم الحصول عليها ، بالإضافة لما رأيناه . هو وضع مكان تخزين الملفات المرفوعة ، خارج نطاق موقعكم . هذه الإمكانية ، غالبا متاحة للذين يتوفرون على خادومهم الخاص عند المستضيف "Dedicated server" . كما يجب كتابة سكريبت للسمحاح بالتعامل مع هذه الملفات من داخل الموقع .
  • تغيير رخص ملف "uploads" بحيث يجب ألا نسمح بتنفيذ السكريبتات . في هذه الحالة ، حتّى إذا تمكن المهاجم من رفع ملفه ، فلن يتمكن من تنفيذه . توجد طرق عديدة لفعل ذلك . سنعتمد على إنشاء صفحة "htaccess." على برنام "++Notepad" مثلا . و نضع فيها الشيفرة التالية . هذه الشيفرة ستمنع من تنفيذ سكريبتات php :

    php_flag engine off

    ثم سنقوم بحفظ الصفحة في مكان ، بحيث لا يجب أن تتواجد في نفس المكان مع الملفات المرفوعة . و إلا قد يتمكن المهاجم من رفع ملف "htaccess." و يبدل ملفكم الأصلي بملفه . لهذا إذا كنتم تضعون الملفات المرفوعة مباشرة داخل ملف "uploads" . يجب أن تحفظوا صفحة "htaccess." خارج ملف "uploads" . في هذه الحالة ، احذروا ، فجميع الملفات التي ستتواجد في نفس المستوى مع ملف "uploads" ستُأثر عليها صفحة "htaccess."

    إذا حصلتم على خطأ "ERROR 500" ، عدّلوا الشيفرة كالآتي :
    <IfModule mod_php5.c>
        php_flag engine off
    </IfModule>
    

    إذا كان متاح لكم الولوج إلى ملف httpd.conf على خادومكم ، بدل إنشاء صفحة "htaccess." السابقة ، أضيفوا فقط الشيفرة أسفله ل virtualhost مع تعديل الطريق إلى ملف "uploads"
    <VirtualHost *:80>
        # ...
        <Directory /path/to/webroot/to/uploads>
            deny from all
            <Files ~ "^\w+\.(gif|jpe?g|png)$">
                order deny,allow
                allow from all
            </Files>  
            <IfModule mod_php5.c>
                php_flag engine off
            </IfModule>      
        </Directory>
    </VirtualHost>
    

    هذه الطرق كلها تصب في مقاربة واحدة ، هي أنه حتّى إذا تمكّن المُهاجم من رفع الشال فلن يتمكّن من تنفيذه .
  • يمكننا أيضا حفظ الملفات خارج نطاق الموقع ، بتخزينها في قاعدة البيانات ، باستعمال حقل من نوع "Blob". و لن نحتاج هنا إلى ملف "uploads" . لكن هذه الطريقة ستُثقل كاهل قاعدة البيانات ، نظرا لأحجام الملفات . غالبا لا تُستعمل و ليست مُحبذة ، بخلاف ذلك سنتعرف في الدرس الموالي بتخزين بعض البيانات الخفيفة للملف . ليسهل علينا التعامل معه
  • بالإضافة إلى كل هذا ، يجب أن تقوموا بحماية جميع استماراتكم من ثغرات XSS و CSRF . بالنسبة للثغرة الأولى ، تعرفنا عليها سابقا . أما بالنسبة لثغرة CSRF فهذا موضوع ليوم آخر ، بحول الله . أمّا بالنسبة لثغرة "حقن sql" . إذا كنتم تتعاملون مع قاعدة البيانات باستعمال تهييئ الإستعلام "PDO::prepare"، كما رأينا سابقا في هذا الملف . فأنتم محميون من هذه الثغرة .

أنهي هذا الدّرس ، بإعطائكم مثالا لطريقة معالجة الأخطاء .

<?php
$error = '';

if(!preg_match('#^[a-zA-Z0-9_-]{1,200}\.(jpe?g|gif|png)$#', $_FILES['milaf']['name'])) 
{
  $error .= '<p>إسم الملف غير صحيح </p>';
}
// معالجة باقي الأخطاء
// ...

if ($error == '') // إذا لم نسجل أي خطأ ، نسمح برفع الملف
{ 
   // رفع الملف
} else {
  // عرض الأخطاء
   echo '<h3> تم رفض رفع الملف للأسباب التالية : </h3>';
   if($error != '') echo $error;
}
?>

أحثكم على تحسين هذا السكريبت بإنشاء دالة . أو مصفوفة أحسن إن كنتم تستخدمون "POO".