Безопасная заливка файлов на сайт. Одно из тысячи решений

в 14:56, , рубрики: php, upload, безопасность, скрипты, метки: , ,

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

Как Вы все уже хорошо знаете, самый надежный способ защиты при заливке файлов, это:

1. Переименовывать файлы в имена и расширения файлов независимо от входящего имени файла.
2. Отключить в папке, куда заливаются файлы, выполнение скриптов.

Этот вариант отлично подходит для большинства случаев, когда заливаемый файл нужно использовать на сайте (например: аватар пользователя) и он требует записи в БД для привязки. Реализуется легко и об этом уже на Хабре есть статьи.

Мы же с Вами рассмотрим ситуацию, когда нужно сделать безопасную заливку файлов, без использования БД, с сохранением оригинальных имен и с возможностью заливать что угодно (без каких либо ограничений) с последующей возможностью скачивания данных файлов по URL.

Для лучшего восприятия сейчас реализуем примитивный файлообменник. Для этого нам понадобится: папка, назовем ее, например «files», и 2 скрипта «upload.php» и «get.php».

В скрипте «upload.php» реализуем заливку файла с преобразованием имени в hex-код. Т.е. например, если пользователь решил залить файл «index.php» то он нормально зальется в папку, но с именем «696e6465782e706870» что полностью безопасно для сервера и не выполнится PHP-обработчиком.

Вот скрипт «upload.php»:

<?
$uploaddir = "files/";

function StringToHex($s)
{
    $hex = "";
    for ($i=0;$i<strlen($s);$i++) $hex=$hex.dechex(ord($s[$i]));
    return $hex;
}

if (isset($_FILES['userfile']))
{
	// Преобразуем имя файла в hex
	$new_name = StringToHex(strtolower($_FILES['userfile']['name']));
	if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploaddir . $new_name))
	{
		// Ok...
	} else {
		// Error...
	}
}
?>
<html>
<body>
<form enctype="multipart/form-data" method="post">
<input name="userfile" type="file" /><input type="submit" value="Сохранить" />
</form>
</body>
</html>

Сейчас займемся папкой, куда будут попадать файлы. Первым делом поставим ей права 777 и создадим в ней файл «.htaccess»:

RewriteEngine on
RewriteRule (.*) get.php?file=$1 [L,QSA]

Ну и осталось добавить в папку «files» скрипт «get.php», который будет отдавать файлы пользователю, для наглядности я в него добавил возможность просмотра файлов в папке с их реальными именами (ответ для тех читателей, которые спросят типа «а зачем все это было, если можно было имена шифровать в md5» и.т.д.).

<?
function StringToHex($s)
{
    $h = "";
    for ($i=0;$i<strlen($s);$i++) $h=$h.dechex(ord($s[$i]));
    return $h;
}

function HexToString($h)
{
    $s = "";
    for ($i=0;$i<strlen($h)-1;$i+=2) $s=$s.chr(hexdec($h[$i].$h[$i+1]));
    return $s;
}

if (empty($_GET["file"]))
{
	// Для наглядности (вывод содержимого папки)
	if ($dh = opendir("."))
	while (($file = readdir($dh)) !== false)
	{
		if (preg_match("/^[a-z0-9]+$/", $file))
		echo '<a href="',HexToString($file),'">',HexToString($file),'</a><br />';
	}
	closedir($dh);
	exit;
}

$file_to_user = $_GET["file"];
$file_name = StringToHex(strtolower($_GET["file"]));

if (file_exists($file_name))
{
	// Если имя файла поле преобразования совпало с существующим файлом - отдадим его пользователю на скачивание
	if (ob_get_level()) ob_end_clean();	
	header('Content-Description: File Transfer');
	header('Content-Type: application/octet-stream');
	header('Content-Disposition: attachment; filename='.basename($file_to_user));
	header('Content-Transfer-Encoding: binary');
	header('Expires: 0');
	header('Cache-Control: must-revalidate');
	header('Pragma: public');
	header('Content-Length: '.filesize($file_name));
	readfile($file_name);
}
?>

Таким образом, для пользователей сайта всегда будут доступны файлы (на скачивание), залитые в папку по нормальным ссылкам:

http://site.xx/files/img.gif
http://site.xx/files/index.php
http://site.xx/files/jquery.js
http://site.xx/files/readme.txt

В реальности же в папке «files» будут файлы с именами:

696e6465782e706870
696d672e676966
6a71756572792e6a73
726561646d652e747874

Ну, вот собственно и все. Естественно есть куча других вариантов реализации защищенной заливки, особенно красиво все можно реализовать с использованием БД, я же решил предоставить свой метод приема/отдачи файлов в их естественном виде, без запретов на скрипты, и при этом с безопасностью для сайта. Для более высокой защиты (например: от повторных имен файлов, и.т.д.) скрипты нужно улучшать, тут только базовая идея реализована.

Автор: bmmshayan

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js