Безопасность при загрузке файлов php

How to securely upload files with PHP?

Uploading files in PHP is achieved with the move_uploaded_file() function.

The HTML form for uploading single or multiple files must include the enctype=»multipart/form-data» attribute. Use the POST method:

 method="post" enctype="multipart/form-data" action="upload.php"> File:  type="file" name="pictures[]" multiple="true">  type="submit">  

And the PHP upload.php script looks like the following:

 foreach ($_FILES['pictures']['error'] as $key => $error)  if ($error == UPLOAD_ERR_OK)  $tmpName = $_FILES['pictures']['tmp_name'][$key]; // basename() may prevent directory traversal attacks, but further // validations are required $name = basename($_FILES['pictures']['name'][$key]); move_uploaded_file($tmpName, "/var/www/project/uploads/$name"); > > 

Don’t stop here just yet and continue reading! The uploaded files must be validated for security purposes. A lot of hacks can occur when uploading hasn’t been properly secured. Imagine a malicious attacker uploads evil.php which is publicly accessible over https://example.com/uploads/evil.php !

Validation

Always make sure that you implement server-side validation in order to be able to upload securely, and make sure that you understand the reasons for this, and the security vulnerabilities that you would otherwise be exposed to.

Directory traversal

To avoid directory traversal (a.k.a. path traversal) attacks, use basename() like shown above, or even better, rename the file completely like in the next step.

Rename uploaded files

Renaming uploaded files avoids duplicate names in your upload destination, and also helps to prevent directory traversal attacks. If you need to keep the original filename, you can it in a database for retrieval in the future. As an example, renaming a file with microtime() and some random number:

$uploadedName = $_FILES['upload']['name']; $ext = strtolower(substr($uploadedName, strripos($uploadedName, '.')+1)); $filename = round(microtime(true)).mt_rand().'.'.$ext; 

You can also use hashing functions like hash_file() and sha1_file() to build filenames. This method can save some storage space when different users upload the same file.

$uploadedName = $_FILES['upload']['name']; $ext = strtolower(substr($uploadedName, strripos($uploadedName, '.')+1)); $filename = hash_file('sha256', $uploadedName) . '.' . $ext; 

Check file type

Instead of relying on file extensions, you can get the mime-type of a file with finfo_file():

$finfo = finfo_open(FILEINFO_MIME_TYPE); // return mime-type extension echo finfo_file($finfo, $filename); finfo_close($finfo); 

For images, a check that’s more reliable, but still not really good enough is using the getimagesize() function:

$size = @getimagesize($filename); if (empty($size) || ($size[0] === 0) || ($size[1] === 0))  throw new \Exception('Image size is not set.'); > 

Check file size

Checking for uploaded file size is important to not overload the server with too big file(s). When checking uploaded file size there are several main levels to look into.

The upload_max_filesize ini directive

The most important is to limit the upload_max_filesize and post_max_size ini directives in the php.ini file. This prevents the server disk size from being overloaded on the server. It stops uploading as soon as the upload_max_filesize is reached and sets the UPLOAD_ERR_INI_SIZE error code to the $_FILES[‘key’][‘error’] . If the post_max_size has been reached the $_POST and $_FILES will be empty.

Checking uploaded file size in the code

Second most important beside above is to also check the uploaded file size in the application code using either filesize($_FILES[‘key’][‘tmp_name]) function or the $_FILES[‘files’][‘size’] . Both are equally valid when uploading files is concerned.

To limit or check the size of the uploaded file from the $_FILES[‘key’][‘size’] :

if ($_FILES['pictures']['size'] > 1000000)  throw new Exception('Exceeded file size limit.'); > 

Client side form validation (HTML5 or JavaScript)

To check the uploaded file size on the client side is optional and not safe to rely on, yet it improves the user experience.

The MAX_FILE_SIZE hidden field

Then there is also additional PHP specific optional check of using a special hidden field with name MAX_FILE_SIZE (or max_file_size it is case insesitive) in the HTML form that PHP can use. However, it can be spoofed the same way as the client side validation by the evil client side so it is not reliable. It is more of an user experience improvement in cases where very large files are uploaded for example videos.

For example, the following form will limit the file size to 1MB or 1048576 bytes (1*1024*1024):

 method="post" enctype="multipart/form-data" action="upload.php">  type="hidden" name="max_file_size" value="2097152"> File:  type="file" name="pictures[]" multiple="true">  type="submit">  

The max_file_size hidden field needs to be added before the file input field to be effective on the PHP side.

It limits the upload process in the following way:

1.) PHP first checks if the current uploaded bytes is bigger than the upload_max_filesize ini directive.

2.) If not, it additionally checks if the max_file_size field has been defined and if the current uploaded bytes are bigger than it. If it is it interrupts the upload process and sets the UPLOAD_ERR_FORM_SIZE error code in the $_FILES[‘key’][‘error’] . This way user don’t need to wait for the 100% of the file is uploaded but only the max_file_size field value bytes are uploaded and application stops the upload process.

Storing uploads to a private location

Instead of saving uploaded files to a public location available at https://example.com/uploads , storing them in a publicly inaccessible folder is a good practice. To deliver these files, so called proxy scripts are used.

Client-side validation

For better user experience, HTML offers the accept attribute to limit filetypes by the extension or mime-type in the HTML, so users can see the validation errors on the fly and select only allowed filetypes in their browser. However, browser support is limited at the time of writing this. Keep in mind that client-side validation can be easily bypassed by hackers. The server-side validation steps explained above are more important forms of validation to use.

Full example

Let’s take all of the above into consideration and look at a very simple example:

// Check if we've uploaded a file if (!empty($_FILES['upload']) && $_FILES['upload']['error'] == UPLOAD_ERR_OK)  // Be sure we're dealing with an upload if (is_uploaded_file($_FILES['upload']['tmp_name']) === false)  throw new \Exception('Error on upload: Invalid file definition'); > // Rename the uploaded file $uploadName = $_FILES['upload']['name']; $ext = strtolower(substr($uploadName, strripos($uploadName, '.')+1)); $filename = round(microtime(true)).mt_rand().'.'.$ext; move_uploaded_file($_FILES['upload']['tmp_name'], __DIR__.'../uploads/'.$filename); // Insert it into our tracking along with the original name > 

Server configuration

The server-side validation mentioned above can be still bypassed by embedding custom code inside the image itself with tools like jhead, and the file might be ran and interpreted as PHP.

That’s why enforcing filetypes should also be done at the server level.

Apache

Make sure Apache is not configured to interpret multiple files as the same (e.g., images being interpreted as PHP files). Use the ForceType directive to force the type on the uploaded files.

 ForceType application/octet-stream Header set Content-Disposition attachment 
ForceType application/octet-stream ForceType image/jpeg ForceType image/gif ForceType image/png 

Nginx

On Nginx, you can use the rewrite rules, or use the mime.types configuration file provided by default.

location ~* (.*\.pdf)  types  application/octet-stream .pdf; > default_type application/octet-stream; > 

See also

  • How to securely allow users to upload files
  • PHP image upload security: How not to do it
  • Related FAQ: How to increase the file upload size in PHP?
  • PHP Manual: Handling file uploads
  • brandonsavage/Upload — standalone PHP upload component with validation and storage strategies.
  • Uploading files with Laravel framework
  • Uploading files with Symfony framework
  • ralouphie/mimey — PHP package for converting file extensions to MIME types and vice versa.
  • phpMussel/phpMussel — PHP-based anti-virus anti-trojan anti-malware solution capable of scanning file uploads and with some simple upload controls included.

Источник

Загрузка файлов на сервер PHP

Загрузка файлов на сервер PHP

В статье приведен пример формы и php-скрипта для безопасной загрузки файлов на сервер, возможные ошибки и рекомендации при работе с данной темой. HTML-форма отправит файл только методом POST и с атрибутом enctype=»multipart/form-data» .

Форма для загрузки сразу нескольких файлов

Файл upload.php

  • Поддерживает как одиночную загрузку файла так и множественную (multiple) без изменения кода.
  • Проверка на все возможные ошибки которые могут возникнуть при загрузке файлов.
  • Имена файлов переводятся в транслит и удаляются символы которые будут в дальнейшем мешать вывести их на сайте.
  • Есть возможность указать разрешенные и запрещенные для загрузки расширения файлов.
// Название $input_name = 'file'; // Разрешенные расширения файлов. $allow = array(); // Запрещенные расширения файлов. $deny = array( 'phtml', 'php', 'php3', 'php4', 'php5', 'php6', 'php7', 'phps', 'cgi', 'pl', 'asp', 'aspx', 'shtml', 'shtm', 'htaccess', 'htpasswd', 'ini', 'log', 'sh', 'js', 'html', 'htm', 'css', 'sql', 'spl', 'scgi', 'fcgi' ); // Директория куда будут загружаться файлы. $path = __DIR__ . '/uploads/'; if (isset($_FILES[$input_name])) < // Проверим директорию для загрузки. if (!is_dir($path)) < mkdir($path, 0777, true); >// Преобразуем массив $_FILES в удобный вид для перебора в foreach. $files = array(); $diff = count($_FILES[$input_name]) - count($_FILES[$input_name], COUNT_RECURSIVE); if ($diff == 0) < $files = array($_FILES[$input_name]); >else < foreach($_FILES[$input_name] as $k =>$l) < foreach($l as $i =>$v) < $files[$i][$k] = $v; >> > foreach ($files as $file) < $error = $success = ''; // Проверим на ошибки загрузки. if (!empty($file['error']) || empty($file['tmp_name'])) < switch (@$file['error']) < case 1: case 2: $error = 'Превышен размер загружаемого файла.'; break; case 3: $error = 'Файл был получен только частично.'; break; case 4: $error = 'Файл не был загружен.'; break; case 6: $error = 'Файл не загружен - отсутствует временная директория.'; break; case 7: $error = 'Не удалось записать файл на диск.'; break; case 8: $error = 'PHP-расширение остановило загрузку файла.'; break; case 9: $error = 'Файл не был загружен - директория не существует.'; break; case 10: $error = 'Превышен максимально допустимый размер файла.'; break; case 11: $error = 'Данный тип файла запрещен.'; break; case 12: $error = 'Ошибка при копировании файла.'; break; default: $error = 'Файл не был загружен - неизвестная ошибка.'; break; >> elseif ($file['tmp_name'] == 'none' || !is_uploaded_file($file['tmp_name'])) < $error = 'Не удалось загрузить файл.'; >else < // Оставляем в имени файла только буквы, цифры и некоторые символы. $pattern = "[^a-zа-яё0-9,~!@#%^-_\$\?\(\)\\[\]\.]"; $name = mb_eregi_replace($pattern, '-', $file['name']); $name = mb_ereg_replace('[-]+', '-', $name); // Т.к. есть проблема с кириллицей в названиях файлов (файлы становятся недоступны). // Сделаем их транслит: $converter = array( 'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e', 'ё' => 'e', 'ж' => 'zh', 'з' => 'z', 'и' => 'i', 'й' => 'y', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n', 'о' => 'o', 'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'c', 'ч' => 'ch', 'ш' => 'sh', 'щ' => 'sch', 'ь' => '', 'ы' => 'y', 'ъ' => '', 'э' => 'e', 'ю' => 'yu', 'я' => 'ya', 'А' => 'A', 'Б' => 'B', 'В' => 'V', 'Г' => 'G', 'Д' => 'D', 'Е' => 'E', 'Ё' => 'E', 'Ж' => 'Zh', 'З' => 'Z', 'И' => 'I', 'Й' => 'Y', 'К' => 'K', 'Л' => 'L', 'М' => 'M', 'Н' => 'N', 'О' => 'O', 'П' => 'P', 'Р' => 'R', 'С' => 'S', 'Т' => 'T', 'У' => 'U', 'Ф' => 'F', 'Х' => 'H', 'Ц' => 'C', 'Ч' => 'Ch', 'Ш' => 'Sh', 'Щ' => 'Sch', 'Ь' => '', 'Ы' => 'Y', 'Ъ' => '', 'Э' => 'E', 'Ю' => 'Yu', 'Я' => 'Ya', ); $name = strtr($name, $converter); $parts = pathinfo($name); if (empty($name) || empty($parts['extension'])) < $error = 'Недопустимое тип файла'; >elseif (!empty($allow) && !in_array(strtolower($parts['extension']), $allow)) < $error = 'Недопустимый тип файла'; >elseif (!empty($deny) && in_array(strtolower($parts['extension']), $deny)) < $error = 'Недопустимый тип файла'; >else < // Чтобы не затереть файл с таким же названием, добавим префикс. $i = 0; $prefix = ''; while (is_file($path . $parts['filename'] . $prefix . '.' . $parts['extension'])) < $prefix = '(' . ++$i . ')'; >$name = $parts['filename'] . $prefix . '.' . $parts['extension']; // Перемещаем файл в директорию. if (move_uploaded_file($file['tmp_name'], $path . $name)) < // Далее можно сохранить название файла в БД и т.п. $success = 'Файл «' . $name . '» успешно загружен.'; >else < $error = 'Не удалось загрузить файл.'; >> > // Выводим сообщение о результате загрузки. if (!empty($success)) < echo '

' . $success . '

'; > else < echo '

' . $error . '

'; > > >

Возможные проблемы

Оцените статью