Ответ 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 здесь:
Просто прочитайте README.txt и следуйте инструкциям.
Идя с открытым исходным кодом
Проект класса Image Secure теперь также доступен в моем профиле Github. Это сделано для того, чтобы другие (вы?) Могли внести свой вклад в проект и сделать его отличной библиотекой для всех. (в настоящее время прослушивается. Пожалуйста, используйте вышеуказанную загрузку до исправления).