Полная безопасность загрузки изображений Script

Я не знаю, произойдет ли это, но я попробую.

В прошлый час я исследовал безопасность загрузки изображений. Я узнал, что есть много функций для тестирования загрузки.

В моем проекте мне нужно быть в безопасности с загруженными изображениями. Там также может быть очень большой объем, и для этого может потребоваться большая пропускная способность, поэтому покупка API не является вариантом.

Итак, я решил получить полный PHP script для БЕСПЛАТНОЙ безопасной загрузки изображений. Я также думаю, что это поможет многим людям, потому что невозможно найти действительно безопасный. Но я не эксперт по php, поэтому для меня очень важно добавить некоторые функции, поэтому я попрошу эту помощь сообщества создать один полноценный script ДЕЙСТВИТЕЛЬНО безопасного загрузки изображения.

Действительно отличные темы о том, что здесь (но они просто говорят, что нужно для трюка, но не как это сделать, и, как я уже сказал, я не мастер на PHP, поэтому я не могу сделайте это самостоятельно): Список проверки безопасности загрузки изображений PHP https://security.stackexchange.com/info/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>
    

Итак, я прошу помочь, разместив фрагменты кода, которые помогут мне (и всем остальным) сделать эту загрузку изображений script, чтобы сделать ее более безопасной. Или путем совместного использования/создания полного script со всеми добавленными фрагментами.

Ответы

Ответ 1

Когда вы начинаете работать над безопасным сценарием загрузки изображений, нужно учитывать множество факторов. Сейчас я не специалист по этому вопросу, но меня когда-то просили разработать это. Я пройдусь по всему процессу, через который прошел здесь, чтобы вы могли следовать за ним. Для этого я начну с очень простой 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-запросы могут быть перехвачены несколькими способами. Подумайте об аддонах браузера, прокси, скриптах Perl. Как бы мы ни старались, мы не можем помешать злоумышленнику загрузить то, что он не должен делать. Таким образом, вся наша безопасность должна выполняться на стороне сервера.

Первая проблема - это типы файлов. В приведенном выше сценарии злоумышленник может загрузить все, что он хочет, например, скрипт 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.";
}
?>

К сожалению, этого недостаточно. Как я уже упоминал ранее, злоумышленник полностью контролирует запрос. Ничто не помешает ему/ей изменить заголовки запроса и просто изменить тип контента на "image/png". Поэтому вместо того, чтобы полагаться только на заголовок Content-type, было бы лучше также проверить содержимое загруженного файла. Вот где пригодится библиотека 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");
?>

В этом примере мы говорим о многоязычном веб-сайте. Язык сайтов не считается информацией "высокого риска". Мы пытаемся получить предпочтительный язык для посетителей с помощью файла cookie или запроса 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 в нашей базе данных

Нам все еще нужно иметь возможность отображать изображение для посетителей. Мы просто используем столбец id нашей базы данных, чтобы сделать это:

<?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

Просто прочитайте README.txt и следуйте инструкциям.

Идя с открытым исходным кодом
Проект класса Image Secure теперь также доступен в моем профиле Github. Это сделано для того, чтобы другие (вы?) Могли внести свой вклад в проект и сделать его отличной библиотекой для всех. (в настоящее время прослушивается. Пожалуйста, используйте вышеуказанную загрузку до исправления).

Ответ 2

Ну, для загрузки файлов на PHP слишком просто и безопасно. Я рекомендую узнать о:

  • pathinfo - возвращает информацию о пути к файлу
  • move_uploaded_file - перемещает загруженный файл в новое место
  • copy - Копирует файл
  • finfo_open - Создайте новый ресурс fileinfo

Чтобы загрузить файл на PHP, у вас есть два метода: PUT и POST (возможно, больше..). Чтобы использовать метод POST с HTML, необходимо включить enctype в FORM следующим образом:

<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'], YOU_PATH)) {
   // ...
}

И после загрузки файла вам нужно проверить расширение, и лучший и лучший способ использует pathinfo следующим образом:

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

Но расширение не является безопасным, потому что вы можете загружать файл с расширением .jpg, но с mimetype text/php, и это бэкдор. Итак, я рекомендую проверить реальный тип mimetype с помощью finfo_open следующим образом:

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

И, не используйте $_FILES['file']['type'], потому что иногда и в зависимости от браузера и клиентской ОС, вы можете получить application/octet-stream, и этот тип mimetype не является реальным mimetype загруженного вами файла.

Я думаю, что с этим вы можете загрузить файл с безопасностью.

Извини, мой английский, пока!