Представим себе ситуацию, когда клиенту нужно предоставить возможность изменять часть содержимого веб-сайта — например, последние новости об их компании. Одним из способов сделать это состоит в разрешении клиенту загружать текстовые файлы с информацией. Затем эти файлы будут доступны на сайте через шаблон, разработанный на PHP.
Для этого разберемся, как происходит загрузка файлов на сервер.
В PHP доступна очень полезная функциональность — поддержка загрузки файлов через HTTP-протокол. Вместо того, чтобы принимать файлы через HTTP-протокол с сервера в браузер, мы пересылаем их в обратном направлении, то есть с браузера на сервер. Обычно для этого применяются HTML-формы. Форму, которую будем использовать мы, показана на рисунке.
Как видно на рисунке, форма содержит поле ввода, в котором пользователь может ввести имя файла, или щелкнув по кнопке "Обзор", чтобы выбрать файлы, доступные на своей локальной машине. Возможно, вы ранее не видели форму загрузки файлов. Ниже мы покажем как ее реализовать.
После ввода имени файла пользователь может щелкнуть по кнопке "Послать файл", и файл загрузится на сервер, где его ожидает PHP-сценарий.
Для реализации загрузки файлов на сервер применяются некоторые конструкции языка HTML, специально предназначенные для этой цели.
upload.html — HTML-форма для загрузки файлов
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
<title>Администрирование - загрузка новых файлов</title>
</head>
<body>
<h1>Загрузка новых файлов с новостями</h1>
<form action="upload.php" method="post" enctype="multipart/form-data" target="_blank">
<input name="MAX_FILE_SIZE" type="hidden" value="1000000">
Загрузить файл <input name="userfile" type="file">
<input name="" type="submit" value="Послать файл">
</form>
</body>
</html>
Обратите внимание, что в этой форме используется метод POST. Загрузку файлов можно осуществлять с помощью метода PUT, поддерживаемого инструментами Netscape Composer и Amaya, однако при этом придется внести существенные изменения в код. Упомянутые инструменты не поддерживают метод GET.
Рассмотрим особенности этой формы.
Прежде чем двигаться дальше, стоит напомнить, что у некоторых версий PHP в коде загрузки присутствуют бреши в безопасности. Если вы решите пользоваться загрузкой файлов на свой рабочий сервер, нужно удостовериться, что у вас установлена самая последняя версия PHP, и следить за выходом правок и обновлений.
Это не должно стать причиной отказа от такой полезной технологии, но при написании кода нужно проявлять осторожность и стараться ограничить доступ всем, кроме администратора сайта и менеджеров содержимого.
PHP-код загрузки файлов очень прост, однако он зависит от используемой версии PHP и настроек конфигурации. Имена функций и переменных изменяются от версии к версии и зависят от того, включена ли настройка register_globals. Представленный код не требует register_globals, но требует импользования версии PHP не ниже 4.1.
Файл при загрузке помещается в место, отведенное на веб-сервере для временных файлов. По умолчанию это главный временный каталог веб-сервера. Если файл не переименовывать и не переместить до окончания выполнения сценария, он будет уничтожен.
Данные, которые должны обрабатываться в нашем PHP-сценарии, храняться в суперглобальном массиве $_FILES. Если register_globals включен, к данным возможен и непосредственный доступ через имена переменных. Однако здесь, пожалуй, как раз то место, где лучше отключить register_globals и работать с данными через суперглобальный массив.
Элементы в массиве $_FILES будут сохранены с именем дескриптора <file> из вашей HTML-формы. Поскольку элемент формы имеет имя userfile, содержимое массива выглядит следующим образом:
Теперь, когда известно, где находится файл и как он называется, можно скопировать его в более полезное место. Временный файл по окончании выполнения сценария будет удален. Значит, если требуется сохранить файл, его надо переместить или переименовать.
В нашем пимере предполагается, что загружаемые файлы представляют статьи с новостями, поэтому нужно удалить из них все возможные HTML-дескрипторы и перенести в более подходящий каталог.
upload.php — PHP-сценарий приема файла от HTML-формы
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
<title>Загрузка . . .</title>
</head>
<body>
<h1>Загрузка файла . . . </h1>
<?php
if ($_FILES['userfile']['error'] > 0)
{
echo 'Проблема: ';
switch ($_FILES['userfile']['error'])
{
case 1: echo 'размер файла больше uoload_max_filesize' ; break;
case 2: echo 'размер файла больше max_file_size'; break;
case 3: echo 'загружена только часть файла'; break;
case 4: echo 'файл не загружен'; break;
}
exit;
}
// Проверка, имеет ли файл правильный MIME-тип
if ($_FILES ['userfile']['type'] != 'text/plain')
{
echo 'Проблема: файл не является текстовым';
exit;
}
// Помещаем файл туда, куда нужно
$upfile = '/uploads/'.$_FILES['userfile']['name'];
if ($_FILES['userfile']['tmp_name'])
{
if (!move_uploaded_file($_FILES['userfile']['tmp_name'], $upfile))
{
echo 'Проблема: невозможно переместить файл в каталог назначения';
exit;
}
}
else
{
echo 'Проблема: возможна атака через загрузку файла. Файл: ';
echo $_FILES['userfile']['name'];
exit;
}
echo 'Файл успешно загружен. <br><br>';
// Переформатирование содержимого файла
$fp = fopen($upfile, 'r');
$contents = fread ($fp, filesize ($upfile));
fclose ($fp);
$contents = strip_tags ($contents);
$fp = fopen ($upfile, 'w');
fwrite ($fp, $contents);
fclose($fp);
// Вывод загружаемого файла
echo 'Предварительный просмотр содержимого загруженного файла: <br><hr>';
echo $contents;
echo '<br><hr>';
?>
</body>
</html>
Большую часть сценария составляют проверки на предмен возникновения ошибок. Загрузка файла сопряжена с потенциальным риском нарушения безопасности, и этот риск должен быть сведен к минимуму. Нужно как можно более тщательно проверить файл, чтобы убедиться в безопасности его отображения посетителю.
Посмотрим, какие основные части содержит сценарий.
Сначала проверяется код ошибки, возвращаемый в $_FILES['userfile']['error'] . Каждому коду ошибки соответствует специальная константа. Возможные константы и их значения перечислены ниже:
Далее мы проверяем MIME-тип. В данном случае мы решили, что будем загружать только текстовые файлы, поэтому MIME-тип контролируется путем сравнения $_FILES ['userfile']['type'] со строкой 'text/plain'. Это только проверка на предмет ошибки, а не проверка, связанная с безопасностью. MIME-тип определяется браузером пользователя на основе расширения файла, а затем передается серверу. Поскольку достаточно несложно передать ложный MIME-тип, злоумышленники вполне могут воспользоваться этим.
Затем мы проверяем, что файл действительно загружен и не является локальным файлом вроде /etc/passwd. Несколько позже мы вернемся к этому вопросу.
Если все нормально, то мы копируем файл в предназначенный для него каталог. В данном примере это каталог /uploads/ —он находится за пределами дерева веб-документов и потому удобен для помещения в него тех файлов, которые впоследствии будут куда-нибудь включаться.
Затем мы обрабатываем файл, удаляем из него все случайные HTML и PHP-дескрипторы с помощью функции strip_tags() и записываем файл обратно.
И наконец содержимое файла отображается на экране, чтобы пользователь убедился, что загрузка файла успешно завершена.
Результат успешного выполнения сценария выглядит так:
В сентябре 2000 г. появилось сообщение о разработке, при помощи которой взломщики могут переключать сценарий загрузки на обработку локального файла вместо загруженного. Разработка задокументирована в списке рассылки BUGTRAQ. Официальные рекомендации по обеспечению безопасности опубликованы во множестве архивов BUGTRAQ, в частности, на странице http://seclists.org/bugtraq/2000/Sep/index.html.
С целью проверки успешного завершения загрузки файла и гарантии того, что мы имеем дело не с локальным файлом наподобие /etc/passwd, была применена функция move_uploaded_file() и также можно использовать is_uploaded_file().
Небрежно написанный сценарий загрузки может позволить посетителю с недобрыми намерениями создать временный файл с определенным именем и заставить сценарий обрабатывать его как загруженный. А поскольку многие сценарии загрузки возвращают пользователю копию загруженного файла либо размещают в месте, из которого ее можно загрузить, у пользователей появляется возможность доступа к любому файлу, доступному для считывания веб-сервером. Среди этих файлов могут оказаться файлы с конфиденциальной информацией — например, /etc/passwd или файл исходного РНР-кода, содержащий пароли доступа в базу данных.
При загрузке файлов на сервер следует принимать во внимание несколько важных моментов.