Самое главное правило заключается в том, что чужим данным нельзя доверять. Перед тем как вы будете вынуждены их использовать, обязательно проверьте соответствие переданных данных ожидаемому типу и диапазону значений.
Рассмотрим простой пример: пользователь запрашивает страницу сайта, передавая в качестве GET-параметра идентификатор статьи, а ваш сервер делает запрос к базе данных и «вытаскивает» эту статью. То есть адрес выглядит как:
/redir.php?url=www.mysite.ru%2Fpage.php%3Fid%3D3%26nbsp%3B%26nbsp%3B%3Cbr%3E
а запрос к базе в скрипте выполняется в виде:
$SQL = "SELECT * FROM articles WHERE id=$_GET[id]";
Такой пример будет замечательно работать, до тех пор пока кто-то не запросит страницу без параметров — в этом случае переменная $_GET[id] окажется пустой, и запрос к базе выдаст ошибку. Предположим, что вы эту ошибку решите исправить и напишете что-то вроде:
if ($_GET['id'])
$SQL = "SELECT * FROM articles WHERE id=$_GET[id]";
else
$SQL = "SELECT * FROM articles ORDER BY random() LIMIT 1";
Такой скрипт действительно корректно обработает пустой запрос — выдаст случайную статью — но что если посетитель запросит адрес:
/redir.php?url=www.mysite.ru%2Fpage.php%3Fid%3Dvasya%26nbsp%3B%26nbsp%3B%3Cbr%3E
Необходимо проверять и тип и диапазон разрешенных значений данныхСнова база отрапортует об ошибке — несоответствие типа данных. Поэтому надо проверять еще и тип. Практика показывает, что удобнее всего бывает вынести в начало скрипта блок кода, занимающийся инициализацией и проверкой переменных, а в самом скрипте уже использовать только их. Например:
$id = (isset($_GET['id'])) ? (int) $_GET['id'] : 0;
if ($id)
$SQL = "SELECT * FROM articles WHERE id=$id"
else
$SQL = "SELECT * FROM articles ORDER BY random() LIMIT 1";
Переменные надо инициализировать перед использованиемТакая конструкция гарантирует, что переменная $id у вас инициализирована и является целочисленной. Разумеется, она может выходить за пределы имеющихся статей, но тут уже после запроса к базе будет достаточно проверить количество полученных записей — это проще и менее ресурсоемко, чем при каждом вызове страницы проверять, есть ли в базе соответствующая запись.
Однако в некоторых случаях проверка разрешенного диапазона значений является необходимой. Предположим, что у вас есть скрипт, который, в зависимости от переданного пользователем параметра, читает и выдает в браузер какой-то файл:
if (file_exists($_GET['page']))
readfile($_GET['page']);
else
readfile('default.html');
PHP умеет работать с удаленными файламиКазалось бы, здесь защита предусмотрена — вы честно проверили, что запрошенный файл существует. Но на самом деле подобная конструкция является одной из наиболее типичных «уязвимостей». Дело в том, что по умолчанию PHP разрешает использовать в файловых операциях интернет-адреса, то есть пользователь может сделать запрос:
/redir.php?url=www.mysite.ru%2Fscript.php%3Fpage%3Dhttp%253A%252F%252Fdomain.ru%252Fanother_page.htm%26nbsp%3B%26nbsp%3B%26nbsp%3B%26nbsp%3B%3Cbr%3E
и ваш скрипт благополучно прочитает удаленную страницу. Казалось бы, в этом нет ничего особо страшного, но хитрость в том, что если эта удаленная страница содержит PHP-код, то этот чужой код будет выполнен на вашем сервере. А о том, почему это нежелательно, я думаю, рассказывать не обязательно.
Для того чтобы избежать подобной неприятности, вам надо убедиться, что запрошенная страница является корректной. Проще всего это сделать, банально перечислив разрешенные адреса (либо с помощью оператора switch, либо записав их в массив и использовав функцию in_array, либо еще как-то), а если это неприемлемо (например, таких страниц слишком много), то сделайте регекспосвскую проверку или хотя бы воспользуйтесь функцией basename(). А если вашим скриптам не требуется работать с удаленными файлами, то можно запретить PHP это делать, указав в файле php.ini allow_url_fopen = Off.
Бывают и такие ситуации, когда нет возможности проверить ни тип, ни диапазон — например, при добавлении комментариев к статьям пользователь может написать любой текст. Казалось бы, что уж здесь-то никаких опасностей быть не может — пользователь текст написал, вы его сохранили, а потом другому посетителю показали. Но именно здесь кроется еще одна распространенная уязвимость — если текст сохраняется в базе (или вообще как-то используется в SQL-запросах), то посетитель сможет выполнить свои команды! Классический пример — вы запрашиваете имя пользователя и выдаете соответствующие данные:
$SQL = "SELECT * FROM table WHERE username='$_REQUEST[user]'";
Все будет работать замечательно, до тех пор пока кто-нибудь не укажет в качестве имени пользователя что-нибудь вроде:
1'; DELETE * FROM table WHERE '1' = '1
В результате будет выполнено два запроса — первый попробует выбрать пользователя с именем 1, а второй — удалит все записи из базы...
Используйте addslashes() при работе с базой данныхАналогичная проблема может возникнуть и в том случае, если вы выполняете какие-то внешние программы, передавая пользовательские данные в качестве аргументов. Для того чтобы избежать этих неприятностей, обязательно надо использовать функцию addslashes() или ей подобную. Кроме того, для числовых значений стоит принудительно приводить их к требуемому типу c помощью, например, функции settype.
magic_quotes — это не панацеяЕсть в PHP и возможность автоматического преобразования введенных пользователем строк с помощью magiс_quotes — найдите в файле php.ini строку magic_quotes_gpc. Если значение этой переменной равно On, то во всех строках, принимаемых от пользователя, перед всеми символами апострофа, кавычек и обратного слеша (\) автоматически добавится обратный слеш, что «заэкранирует» действие этих символов. Но для полной безопасности этого недостаточно — подобное автоматическое экранирование работает только для пользовательских данных. Но если вы эти данные сохраните в базе (успешно!), а потом в другом запросе используете их, то все слеши пропадут, и вредоносный код успешно сработает! Например:
$name = $_REQUEST['name'];
$id = (int) $_REQUEST['id'];
$SQL = "INSERT INTO table (id, name) VALUES ($id, '$name');
До сих пор все работало правильно. Теперь где-то дальше вы записываете данные в лог:
$user = pg_fetch_result(pg_query($db, "SELECT name FROM table WHERE id = $id"), 0, 0);
$SQL = "INSERT INTO log_table (data, user) VALUES ('now', '$user')";
Служебные заголовки тоже могут использоваться для взлома
И вот тут-то этот вредоносный код и сработает... Поэтому стоит очень внимательно относиться ко всем данным, полученным «извне», и отслеживать их до конца. Хотя можно, разумеется, включить еще и magic_quotes_runtime (по умолчанию эта директива установлена в Off) — при этом PHP будет экранировать все данные, полученные после SQL-запросов или выполнения внешних команд. Надо только не забывать при этом использовать функцию stripslashes(), чтобы убрать лишние слеши при выводе данных.
Если ваш скрипт будет использоваться на нескольких серверах, и вы не знаете, включено ли там автоматическое экранирование, то воспользуйтесь функциями get_magic_quotes_gpc() и get_magic_quotes_runtime() соответственно — в противном случае может получиться так, что либо вы будете дважды экранировать символы, либо не будете их экранировать совсем...
При экранировании данных стоит также учесть и тот факт, что поддельными могут оказаться не только поля вашей формы, но и, скажем, referer, User-Agent и прочие «служебные» заголовки, которые вы можете захотеть сохранить в логе для последующего анализа...
Кстати, если вы показываете в браузере данные, введенные пользователем (например, все те же комментарии), то не забывайте использовать htmlspecialchars(), чтобы удалить (возможно) введенные пользователем теги HTML и JavaScript'ы...
Еще одной распространенной причиной неприятностей является использование register_globals. Эта директива указывается в php.ini и занимается тем, что регистрирует в качестве глобальных переменных данные, полученные от пользователя, с помощью методов GET, POST и COOKIE. Правда, проблемы это вызывает только в том случае, если у вас нет привычки инициализировать все переменные перед их использованием. Например, есть у вас администраторский скрипт:
if ($_REQUEST['user'] == 'admin' and $_REQUEST['password'] == 'my_very_secret_password') $admin=1;
if ($admin) {
// выполнить администраторские обязанности
} else {
// выгнать пользователя
}
Все будет работать замечательно, до тех пор пока кто-нибудь не запросит:
/redir.php?url=www.mysite.ru%2Fadmin.php%3Fadmin%3D1%26nbsp%3B%26nbsp%3B%3Cbr%3E
И еще раз: инициализируйте переменные!Как только это случится, PHP автоматически присвоит переменной $admin значение 1 (оно передано через GET), а ваш скрипт успешно обойдет первую проверку — ведь ни имя ни пароль не были переданы, и разрешит посетителю администрировать ваш сайт...
Для того чтобы этого не происходило, желательно завести привычку всегда инициализировать все используемые переменные. Очень удобно для этого использовать «троичный» оператор:
$var = (условие) ? значение1 : значение2;
Такая конструкция присваивает переменной $var значение1, если условие выполнено, и значение2, если нет. Например:
$page = ($_REQUEST['page']) ? (int) $_REQUEST['page'] : 1;
В этом случае вы твердо уверены, что в переменной $page хранится число, и что если страница не была указана, то переменная установлена в 1. Теперь (почти) без опаски можно использовать эту переменную в запросах к базе. А «почти» заключается в том, что не был проверен диапазон (помните, о чем писалось выше?) — наша страница вполне может оказаться отрицательной или равной нулю... Но это уже не так страшно — в худшем случае, посетитель увидит пустую страницу (так как в базе не найдется записей для отрицательной страницы), если только вы не будете что-нибудь на эту страницу делить.
В дополнение к привычке инициализировать переменные, полезно еще и отключить этот register_globals, а при отладке скриптов включать максимальный уровень предупреждений:
error_reporting(E_ALL);
Добивайтесь, чтобы исчезли не только ошибки, но и предупреждения!На этом уровне PHP будет выдавать большую кучу сообщений о неинициализированных переменных и прочих потенциально опасных вещах, что потребует от вас определенных усилий по их исправлению, зато безопасность ваших скриптов вырастет на порядок. Да и от опечаток вы будете страдать намного меньше... Надо только не забыть отключить сообщения об ошибках при загрузке скриптов на «промышленный» сервер — в противном случае, посетители смогут получить довольно много информации о внутренней структуре вашего сайта... Кстати, очень удобно бывает добавить в начало скрипта что-то вроде:
if (isset($_GET['showerrors']) && $_GET['showerrors'] == 'debug')
error_reporting(E_ALL);
else
error_reporting(0);
В этом случае при необходимости вы сможете включить вывод сообщений об ошибках — это может выручить, если ваш скрипт успешно работает на тестовой машине, но почему-то отказывается работать на сервере...
Статья получена: hostinfo.ru