php - كامل آمن تحميل الصور النصي




image security (2)

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

نموذج HTML:

<form name="upload" action="upload.php" method="POST" enctype="multipart/form-data">
    Select image to upload: <input type="file" name="image">
    <input type="submit" name="upload" value="upload">
</form>

ملف PHP:

<?php
$uploaddir = 'uploads/';

$uploadfile = $uploaddir . basename($_FILES['image']['name']);

if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
    echo "Image succesfully uploaded.";
} else {
    echo "Image uploading failed.";
}
?> 

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

المشكلة الأولى هي أنواع الملفات. في البرنامج النصي أعلاه ، يمكن للمهاجم تحميل أي شيء (أشياء) يريد ، مثل نص php على سبيل المثال ، واتباع رابط مباشر لتنفيذه. لمنع هذا ، ننفذ التحقق من نوع المحتوى :

<?php
if($_FILES['image']['type'] != "image/png") {
    echo "Only PNG images are allowed!";
    exit;
}

$uploaddir = 'uploads/';

$uploadfile = $uploaddir . basename($_FILES['image']['name']);

if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
    echo "Image succesfully uploaded.";
} else {
    echo "Image uploading failed.";
}
?>

لسوء الحظ ، هذا لا يكفي. كما ذكرت من قبل ، يتمتع المهاجم بالسيطرة الكاملة على الطلب. لن يمنعه أي شيء من تعديل رؤوس الطلبات ويغير ببساطة نوع المحتوى إلى "صورة / بابوا نيو غينيا". لذا بدلاً من الاعتماد فقط على رأس نوع المحتوى ، سيكون من الأفضل أيضًا التحقق من صحة محتوى الملف الذي تم تحميله. وإليك أين تأتي مكتبة php GD في متناول يدي. باستخدام getimagesize() ، سنقوم بمعالجة الصورة مع مكتبة GD. إذا لم تكن الصورة ، فسوف تفشل وستفشل عملية التحميل بأكملها:

<?php
$verifyimg = getimagesize($_FILES['image']['tmp_name']);

if($verifyimg['mime'] != 'image/png') {
    echo "Only PNG images are allowed!";
    exit;
}

$uploaddir = 'uploads/';

$uploadfile = $uploaddir . basename($_FILES['image']['name']);

if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
    echo "Image succesfully uploaded.";
} else {
    echo "Image uploading failed.";
}
?>

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

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

order deny,allow
deny from all
allow from 127.0.0.1

تحرير: بعد التحدث مع بعض مبرمجي PHP الآخرين ، أقترح بشدة استخدام مجلد خارج جذر المستند ، لأن htaccess ليس موثوقًا به دائمًا.

ما زلنا بحاجة إلى المستخدم أو أي زائر آخر حتى يتمكن من مشاهدة الصورة. لذلك سوف نستخدم php لاسترداد الصورة لهم:

<?php
$uploaddir = 'uploads/';
$name = $_GET['name']; // Assuming the file name is in the URL for this example
readfile($uploaddir.$name);
?>

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

<?php
if(isset($_COOKIE['lang'])) {
   $lang = $_COOKIE['lang'];
} elseif (isset($_GET['lang'])) {
   $lang = $_GET['lang'];
} else {
   $lang = 'english';
}

include("language/$lang.php");
?>

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

www.example.com/index.php؟lang=../uploads/my_evil_image.jpg

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

الحل لهذه المشكلة هو التأكد من أن المستخدم لا يعرف اسم الملف على الخادم. بدلاً من ذلك ، سنقوم بتغيير اسم الملف وحتى الملحق باستخدام قاعدة بيانات لتتبعه:

CREATE TABLE `uploads` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(64) NOT NULL,
    `original_name` VARCHAR(64) NOT NULL,
    `mime_type` VARCHAR(20) NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;


<?php

if(!empty($_POST['upload']) && !empty($_FILES['image']) && $_FILES['image']['error'] == 0)) {

    $uploaddir = 'uploads/';

    /* Generates random filename and extension */
    function tempnam_sfx($path, $suffix){
        do {
            $file = $path."/".mt_rand().$suffix;
            $fp = @fopen($file, 'x');
        }
        while(!$fp);

        fclose($fp);
        return $file;
    }

    /* Process image with GD library */
    $verifyimg = getimagesize($_FILES['image']['tmp_name']);

    /* Make sure the MIME type is an image */
    $pattern = "#^(image/)[^\s\n<]+$#i";

    if(!preg_match($pattern, $verifyimg['mime']){
        die("Only image files are allowed!");
    }

    /* Rename both the image and the extension */
    $uploadfile = tempnam_sfx($uploaddir, ".tmp");

    /* Upload the file to a secure directory with the new name and extension */
    if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {

        /* Setup a database connection with PDO */
        $dbhost = "localhost";
        $dbuser = "";
        $dbpass = "";
        $dbname = "";

        // Set DSN
        $dsn = 'mysql:host='.$dbhost.';dbname='.$dbname;

        // Set options
        $options = array(
            PDO::ATTR_PERSISTENT    => true,
            PDO::ATTR_ERRMODE       => PDO::ERRMODE_EXCEPTION
        );

        try {
            $db = new PDO($dsn, $dbuser, $dbpass, $options);
        }
        catch(PDOException $e){
            die("Error!: " . $e->getMessage());
        }

        /* Setup query */
        $query = 'INSERT INTO uploads (name, original_name, mime_type) VALUES (:name, :oriname, :mime)';

        /* Prepare query */
        $db->prepare($query);

        /* Bind parameters */
        $db->bindParam(':name', basename($uploadfile));
        $db->bindParam(':oriname', basename($_FILES['image']['name']));
        $db->bindParam(':mime', $_FILES['image']['type']);

        /* Execute query */
        try {
            $db->execute();
        }
        catch(PDOException $e){
            // Remove the uploaded file
            unlink($uploadfile);

            die("Error!: " . $e->getMessage());
        }
    } else {
        die("Image upload failed!");
    }
}
?>

حتى الآن قمنا بما يلي:

  • لقد أنشأنا مكانًا آمنًا لحفظ الصور
  • لقد عالجنا الصورة باستخدام مكتبة GD
  • لقد فحصنا نوع الصورة MIME
  • قمنا بإعادة تسمية اسم الملف وقمنا بتغيير الامتداد
  • لقد قمنا بحفظ كل من اسم الملف الجديد والأصلي في قاعدة البيانات الخاصة بنا
  • لقد حفظنا أيضًا نوع MIME في قاعدة البيانات الخاصة بنا

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

<?php

$uploaddir = 'uploads/';
$id = 1;

/* Setup a database connection with PDO */
$dbhost = "localhost";
$dbuser = "";
$dbpass = "";
$dbname = "";

// Set DSN
$dsn = 'mysql:host='.$dbhost.';dbname='.$dbname;

// Set options
$options = array(
    PDO::ATTR_PERSISTENT    => true,
    PDO::ATTR_ERRMODE       => PDO::ERRMODE_EXCEPTION
);

try {
    $db = new PDO($dsn, $dbuser, $dbpass, $options);
}
catch(PDOException $e){
    die("Error!: " . $e->getMessage());
}

/* Setup query */
$query = 'SELECT name, original_name, mime_type FROM uploads WHERE id=:id';

/* Prepare query */
$db->prepare($query);

/* Bind parameters */
$db->bindParam(':id', $id);

/* Execute query */
try {
    $db->execute();
    $result = $db->fetch(PDO::FETCH_ASSOC);
}
catch(PDOException $e){
    die("Error!: " . $e->getMessage());
}

/* Get the original filename */
$newfile = $result['original_name'];

/* Send headers and file to visitor */
header('Content-Description: File Transfer');
header('Content-Disposition: attachment; filename='.basename($newfile));
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($uploaddir.$result['name']));
header("Content-Type: " . $result['mime_type']);
readfile($uploaddir.$result['name']);
?>

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

وهذا يخلص برنامجي الآمن لتحميل الصور.

أود أن أضيف أنني لم أدرج الحد الأقصى لحجم الملف في هذا البرنامج النصي ، لكن يجب أن تكون قادرًا على القيام بذلك بنفسك بسهولة.

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

نظرًا لأن الكود كبير الحجم للنشر هنا ، يمكنك تنزيل الفصل من MEGA هنا:

قم بتنزيل ImageUpload Class

فقط اقرأ README.txt واتبع التعليمات.

الذهاب مفتوحة المصدر
يتوفر الآن برنامج Image Secure class في ملفي الشخصي على Github . هذا حتى يتمكن الآخرون (أنت؟) من المساهمة في المشروع وجعل هذه المكتبة رائعة للجميع. (تم التنبيه حاليًا. الرجاء استخدام التنزيل أعلاه حتى يصلح للثبات).

لا أعرف إذا كان هذا سيحدث ، لكنني سأحاول ذلك.

خلال الساعة الماضية ، أجريت أبحاثًا حول سلامة تحميل الصور. تعلمت أن هناك الكثير من الوظائف لاختبار التحميل.

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

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

توجد موضوعات رائعة حقًا حول هذا الموضوع (ومع ذلك ، فهي تخبر فقط ما هو مطلوب للقيام بالخدعة ، ولكن ليس كيفية القيام بذلك ، وكما قلت أنني لست على درجة الماجستير في PHP ، لذلك أنا غير قادر على القيام بهذا كله بنفسي): قائمة فحص أمان تحميل صورة PHP https://security.stackexchange.com/questions/32852/risks-of-a-php-image-upload-form

باختصار ، يقولون أن هذا هو المطلوب لتحميل صورة الأمان (سأقتبس من الصفحات أعلاه):

  • تعطيل PHP من التشغيل داخل مجلد التحميل باستخدام .httaccess.
  • لا تسمح بالتحميل إذا كان اسم الملف يحتوي على السلسلة "php".
  • السماح فقط بالامتدادات: jpg ، jpeg ، gif و png.
  • اسمح فقط بنوع ملف الصورة.
  • عدم السماح للصورة بنوعين من الملفات.
  • تغيير اسم الصورة. تحميل إلى دليل فرعي وليس دليل الجذر.

أيضا:

  • أعد معالجة الصورة باستخدام GD (أو Imagick) وحفظ الصورة التي تمت معالجتها. جميع الآخرين مجرد متعة مملة للمتسللين "
  • كما أوضح rr ، استخدم move_uploaded_file () لأي تحميل "
  • بالمناسبة ، تريد أن تكون مقيدًا جدًا بشأن مجلد التحميل. هذه الأماكن هي واحدة من الزوايا المظلمة حيث يستغل الكثير
    يحدث. هذا صحيح لأي نوع من التحميل وأي برمجة
    اللغة / الخادم. التحقق من
    https://www.owasp.org/index.php/Unrestricted_File_Upload
  • المستوى 1: تحقق من الامتداد (ينتهي ملف التمديد بـ)
  • المستوى 2: تحقق من نوع MIME ($ file_info = getimagesize ($ _ FILES ['image_file'] ؛ $ file_mime = $ file_info ['mime'] ؛)
  • المستوى 3: اقرأ أول 100 بايت وتحقق مما إذا كانت تحتوي على أي بايت في النطاق التالي: ASCII 0-8 ، 12-31 (عشري).
  • المستوى 4: التحقق من وجود أرقام سحرية في الرأس (أول 10-20 بايت من الملف). يمكنك العثور على بعض بايتات رأس الملفات من هنا:
    http://en.wikipedia.org/wiki/Magic_number_%28programming%29#Examples
  • قد ترغب في تشغيل "is_uploaded_file" على $ _FILES ['my_files'] ['tmp_name'] أيضًا. نرى
    http://php.net/manual/en/function.is-uploaded-file.php

إليك جزء كبير منه ، ولكن لا يزال هذا ليس كل شيء. (إذا كنت تعرف شيئًا أكثر قد يساعد في جعل التحميل أكثر أمانًا ، فيرجى مشاركته.)

هذا ما حصلنا عليه الآن

  • PHP الرئيسي:

    function uploadFile ($file_field = null, $check_image = false, $random_name = false) {
    
    //Config Section    
    //Set file upload path
    $path = 'uploads/'; //with trailing slash
    //Set max file size in bytes
    $max_size = 1000000;
    //Set default file extension whitelist
    $whitelist_ext = array('jpeg','jpg','png','gif');
    //Set default file type whitelist
    $whitelist_type = array('image/jpeg', 'image/jpg', 'image/png','image/gif');
    
    //The Validation
    // Create an array to hold any output
    $out = array('error'=>null);
    
    if (!$file_field) {
      $out['error'][] = "Please specify a valid form field name";           
    }
    
    if (!$path) {
      $out['error'][] = "Please specify a valid upload path";               
    }
    
    if (count($out['error'])>0) {
      return $out;
    }
    
    //Make sure that there is a file
    if((!empty($_FILES[$file_field])) && ($_FILES[$file_field]['error'] == 0)) {
    
    // Get filename
    $file_info = pathinfo($_FILES[$file_field]['name']);
    $name = $file_info['filename'];
    $ext = $file_info['extension'];
    
    //Check file has the right extension           
    if (!in_array($ext, $whitelist_ext)) {
      $out['error'][] = "Invalid file Extension";
    }
    
    //Check that the file is of the right type
    if (!in_array($_FILES[$file_field]["type"], $whitelist_type)) {
      $out['error'][] = "Invalid file Type";
    }
    
    //Check that the file is not too big
    if ($_FILES[$file_field]["size"] > $max_size) {
      $out['error'][] = "File is too big";
    }
    
    //If $check image is set as true
    if ($check_image) {
      if (!getimagesize($_FILES[$file_field]['tmp_name'])) {
        $out['error'][] = "Uploaded file is not a valid image";
      }
    }
    
    //Create full filename including path
    if ($random_name) {
      // Generate random filename
      $tmp = str_replace(array('.',' '), array('',''), microtime());
    
      if (!$tmp || $tmp == '') {
        $out['error'][] = "File must have a name";
      }     
      $newname = $tmp.'.'.$ext;                                
    } else {
        $newname = $name.'.'.$ext;
    }
    
    //Check if file already exists on server
    if (file_exists($path.$newname)) {
      $out['error'][] = "A file with this name already exists";
    }
    
    if (count($out['error'])>0) {
      //The file has not correctly validated
      return $out;
    } 
    
    if (move_uploaded_file($_FILES[$file_field]['tmp_name'], $path.$newname)) {
      //Success
      $out['filepath'] = $path;
      $out['filename'] = $newname;
      return $out;
    } else {
      $out['error'][] = "Server Error!";
    }
    
     } else {
      $out['error'][] = "No file uploaded";
      return $out;
     }      
    }
    
    
    if (isset($_POST['submit'])) {
     $file = uploadFile('file', true, true);
     if (is_array($file['error'])) {
      $message = '';
      foreach ($file['error'] as $msg) {
      $message .= '<p>'.$msg.'</p>';    
     }
    } else {
     $message = "File uploaded successfully".$newname;
    }
     echo $message;
    }
  • والشكل:

    <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post" enctype="multipart/form-data" name="form1" id="form1">
    <input name="file" type="file" id="imagee" />
    <input name="submit" type="submit" value="Upload" />
    </form>

لذا ، فإن ما أطلبه هو المساعدة من خلال نشر قصاصات من الرموز التي ستساعدني (ولكل شخص آخر) في جعل برنامج تحميل الصور هذا آمنًا للغاية. أو من خلال مشاركة / إنشاء برنامج نصي كامل مع جميع المقتطفات المضافة.


لتحميل الملفات في PHP سهل وآمن. أوصي بمعرفة:

لتحميل ملف في PHP لديك طريقتان: PUT و POST . لاستخدام أسلوب POST مع HTML ، يلزمك تمكين enctype في النموذج الخاص بك مثل هذا:

<form action="" method="post" enctype="multipart/form-data">
  <input type="file" name="file">
  <input type="submit" value="Upload">
</form>

ثم في PHP تحتاج إلى الحصول على الملف الذي تم تحميله مع $_FILES مثل هذا:

$_FILES['file']

فأنت بحاجة إلى نقل الملف من temp ("upload") مع move_uploaded_file :

if (move_uploaded_file($_FILES['file']['tmp_name'], YOUR_PATH)) {
   // ...
}

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

$extension = pathinfo($_FILES['file']['tmp_name'], PATHINFO_EXTENSION);

لكن الملحق غير آمن لأنه يمكنك تحميل ملف به ملحق .jpg ولكن مع mimetype text/php وهذا مستتر. لذلك ، أوصي بالتحقق من النوع الحقيقي مع finfo_open مثل هذا:

$mime = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $_FILES['file']['tmp_name']);

ولا تستخدم $_FILES['file']['type'] لأنه في بعض الأحيان ، اعتمادًا على متصفحك ونظام التشغيل العميل ، قد تتلقى application/octet-stream وهذا النوع الصغير ليس هو النوع الحقيقي للملف الذي قمت بتحميله.

أعتقد أنه يمكنك تحميل الملفات بشكل آمن باستخدام هذا السيناريو.

آسف لغتي الإنجليزية ، وداعا!







upload