Данный материал для начинающих программистов.
Почему так часто я вижу, зайдя на какой-нибудь сайт что-то подобное этому:
Это одна из стандартных PHP ошибок, которая а) некрасива для пользователя; б) потенциально опасна.
Поэтому их необходимо перехватывать и упорядочивать.
Во первых, функция error_reporting позволяет нам решить, какие ошибки мы хотим видеть.
В принципе, достаточно просто выключить показ всех ошибок (error_reporting(0)), но нам нужно не это, потому что об ошибках мы хотим знать.
Константа всех ошибок — E_ALL.
В пятой версии появилась константа E_STRICT, показывающая строгие замечания по поводу кода.
Разумеется, их желательно видеть, но они не входят в E_ALL, потому будем использовать числовое значение error_reporting(8191), которое вбирает всё, вплоть до новых ошибок шестой версии.
Примечание для любознательных: error_reporting(E_ALL | E_STRICT) не подходит, ибо тогда PHP 4 будет ругаться, не зная, что такое E_STRICT. С численным значением никаких проблем не будет.
Добавляем проверку на DEBUG — константу, выставленной в конфиге, и, с помощью set_error_handler, будем отлавливать ошибки в уже запущеном сервисе. Кстати, свой репортер ошибок должен возвращать true, иначе PHP выбросит стандартную ошибку.
Результат:
(Насчёт сравнения переменной с пятью параметрами я не уверен в выборе метода: in_array красивее, и гораздо медленее, а switch case case быстрее, но совсем некрасиво. Красота — субъективное дело...)
До версии 4.2.0 директива register_globals была в PHP включена по умолчанию.
Привело это к тому, что многие привыкли, что если в форме есть , то в PHP коде можно проверять if ($username == 'admin')…
Однако это потенциальная дыра, которая привела ко множеству взломов.
Поэтому к POST, GET, COOKIE переменным надо обращаться через superglobals $_POST, $_GET, $_COOKIE.
Многим это показалось слишком трудно и стала очень популярной команда import_request_variables, возвращающая всё на круги своя.
Так вот.
Не делайте этого.
Другая проблема с register_globals:
Если пользователь — не администратор, а переменная $user_level не инициализирована
(ей не придано значение 0 в начале скрипта, в надежде, что оно 0 автоматически),
то нехороший человек может дописать в адресной строке foo.php?user_level=999 и получить доступ.
Так популярна среди начинающих конструкция
опасна. Если пользователь введёт вместо пароля ' OR `username` = 'admin, то система впустит его как админа.
Приведённый пример, разумеется, элементарен.
Но если не решить проблему глобально, всегда можно пропустить какой-нибудь запрос, подверженный SQL injection.
Для борьбы с этим разработчики PHP решили сделать так, чтобы вся информация, поступающая от пользователя, подвергалась обработке и все кавычки escapeились (перед ними ставится слэш, что делает команда addslashes).
Что случилось? Вся информация от пользователя приходит со слэшами. Даже та, что вроде слэши получить не должна. Например, комментарии к статье.
Мало того, это не 100-процентный способ защиты от SQL injection.
Решение. а) со всей входящей информации снимаеи слэши, если они есть. б) Всю информацию, поступающую в SQL запрос фильтруем специально для этого созданной функцией mysql_real_escape_string (или аналогом для другой базы данных).
Снимаем слэши:
Создаём функцию для фильтрации (mysql_real_escape_string — длинно, да и привязанно к формату проверки. А если понадобиться поменять фильтр?)
И используем её везде. Как только какие-то динамические данных отсылаются SQLу, сразу используем quote:
Проверяйте всё, что вводит пользователь.
По умолчанию он злоумышленник.
Старайтесь не фильтровать, старайтесь валидировать.
Другими словами, не создавайте чёрного списка, создавайте белый.
Вместо
используйте
Так вы будете уверены, что информация чиста и никаких неожиданностей не будет.
Если вы составляете список запрещённых символов, всегда можете просмотреть какой-нибудь нехороший %00 и подобные, о которых, скорее всего, не догадываетесь.
Разумеется, есть ситуации, когда нужна фильтрация, например, когда пользователь пишет комментарий.
Тогда надо отсекать плохие символы.
Но в принципе стараться надо валидировать.
Есть несколько команд, с которыми надо обращаться очень осторожно.
Это include, require, readfile, eval, ``, system, exec, create_function, dir, fopen и подобные.
Всегда посмотрите трижды, когда используете их, если в них используются данные, которые могут прийти от пользователя, будьте уверены — кто-то обязательно этим воспользуется.
Этот кусок опасен. Если злоумышленник введёт '../../../../../etc/passwd%00', будет рад, а вы — вряд-ли.
Не забывайте, что cookies редактируются ни чуть не сложнее, чем то, что видно в адресной строке.
Поэтому всё, что приходит как печенье, потенциально — атака.
Так что не надо хранить в cookies уровень доступа пользователя или его ID.
Лучше всего дать PHP самому разбираться с этим, используя сессии.
Кстати, в cookies вообще хранить что-либо надо очень скромно и три раза подумать, а надо ли?
Всё время думайте о данных в переменных $_GET, $_POST, $_COOKIE, как об атаке злоумышленника.
Trust no one! :)
Содержание
- Демонстрация ошибок
- register_globals
- SQL injection и magic_quotes
- Проверка данных
- Аутентификация
- Вывод
Демонстрация ошибок
Почему так часто я вижу, зайдя на какой-нибудь сайт что-то подобное этому:
Warning: Use of undefined constant LOCAL_SERVER — assumed 'LOCAL_SERVER' in /web/includes/page-definitions.php on line 13
Это одна из стандартных PHP ошибок, которая а) некрасива для пользователя; б) потенциально опасна.
Поэтому их необходимо перехватывать и упорядочивать.
Во первых, функция error_reporting позволяет нам решить, какие ошибки мы хотим видеть.
В принципе, достаточно просто выключить показ всех ошибок (error_reporting(0)), но нам нужно не это, потому что об ошибках мы хотим знать.
Константа всех ошибок — E_ALL.
В пятой версии появилась константа E_STRICT, показывающая строгие замечания по поводу кода.
Разумеется, их желательно видеть, но они не входят в E_ALL, потому будем использовать числовое значение error_reporting(8191), которое вбирает всё, вплоть до новых ошибок шестой версии.
Примечание для любознательных: error_reporting(E_ALL | E_STRICT) не подходит, ибо тогда PHP 4 будет ругаться, не зная, что такое E_STRICT. С численным значением никаких проблем не будет.
Добавляем проверку на DEBUG — константу, выставленной в конфиге, и, с помощью set_error_handler, будем отлавливать ошибки в уже запущеном сервисе. Кстати, свой репортер ошибок должен возвращать true, иначе PHP выбросит стандартную ошибку.
Результат:
(Насчёт сравнения переменной с пятью параметрами я не уверен в выборе метода: in_array красивее, и гораздо медленее, а switch case case быстрее, но совсем некрасиво. Красота — субъективное дело...)
<?php
error_reporting(8191);
if (!DEBUG)
{
function errorHandler ($errno, $errstr, $errfile, $errline)
{
// Запись в БД или отсылка по почте вебмастеру.
if ($errno == E_ERROR ||
$errno == E_PARSE ||
$errno == E_CORE_ERROR ||
$errno == E_COMPILE_ERROR ||
$errno == E_USER_ERROR)
{
// Сообщение пользователю. Мол, «простите, облажались маленько»...
}
return true;
}
set_error_handler('errorHandler');
}
?>
register_globals
До версии 4.2.0 директива register_globals была в PHP включена по умолчанию.
Привело это к тому, что многие привыкли, что если в форме есть , то в PHP коде можно проверять if ($username == 'admin')…
Однако это потенциальная дыра, которая привела ко множеству взломов.
Поэтому к POST, GET, COOKIE переменным надо обращаться через superglobals $_POST, $_GET, $_COOKIE.
Многим это показалось слишком трудно и стала очень популярной команда import_request_variables, возвращающая всё на круги своя.
Так вот.
Не делайте этого.
Другая проблема с register_globals:
<?php
...
if (check_admin($..., $...))
{
...
$user_level = 169;
}
...
if ($user_level > 150)
{
echo 'Boom!';
}
?>
Если пользователь — не администратор, а переменная $user_level не инициализирована
(ей не придано значение 0 в начале скрипта, в надежде, что оно 0 автоматически),
то нехороший человек может дописать в адресной строке foo.php?user_level=999 и получить доступ.
SQL injection и magic_quotes
Так популярна среди начинающих конструкция
<?php
$user = mysql_fetch_assoc(mysql_query("SELECT * FROM `users` WHERE `username` = '{$_POST['username'}' AND `password` = '{$_POST['password']}'"));
?>
опасна. Если пользователь введёт вместо пароля ' OR `username` = 'admin, то система впустит его как админа.
Приведённый пример, разумеется, элементарен.
Но если не решить проблему глобально, всегда можно пропустить какой-нибудь запрос, подверженный SQL injection.
Для борьбы с этим разработчики PHP решили сделать так, чтобы вся информация, поступающая от пользователя, подвергалась обработке и все кавычки escapeились (перед ними ставится слэш, что делает команда addslashes).
Что случилось? Вся информация от пользователя приходит со слэшами. Даже та, что вроде слэши получить не должна. Например, комментарии к статье.
Мало того, это не 100-процентный способ защиты от SQL injection.
Решение. а) со всей входящей информации снимаеи слэши, если они есть. б) Всю информацию, поступающую в SQL запрос фильтруем специально для этого созданной функцией mysql_real_escape_string (или аналогом для другой базы данных).
Снимаем слэши:
<?php
{
if (function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc())
{
function stripslashes_deep($value)
{
if(is_array($value))
{
$value = array_map('stripslashes_deep', $value);
}
elseif (!empty($value) && is_string($value))
{
$value = stripslashes($value);
}
return $value;
}
$_POST = stripslashes_deep($_POST);
$_GET = stripslashes_deep($_GET);
$_COOKIE = stripslashes_deep($_COOKIE);
}
}
?>
Создаём функцию для фильтрации (mysql_real_escape_string — длинно, да и привязанно к формату проверки. А если понадобиться поменять фильтр?)
<?php
function quote($value) {
if (!is_numeric($value)) {
$value = "'".mysql_real_escape_string($value)."'";
}
return $value;
}
?>
И используем её везде. Как только какие-то динамические данных отсылаются SQLу, сразу используем quote:
<?php
$user = mysql_fetch_assoc(mysql_query('SELECT * FROM `users` WHERE `username` = '.quote($_POST['username']).' AND `password` = '.quote($_POST['password'])));
?>
Проверка данных
Проверяйте всё, что вводит пользователь.
По умолчанию он злоумышленник.
Старайтесь не фильтровать, старайтесь валидировать.
Другими словами, не создавайте чёрного списка, создавайте белый.
Вместо
<?php
if (are_bad_symbols($data)) boo();
?>
используйте
<?php
if (!all_good_symbols($data)) boo();
// Например:
is_numeric($data);
preg_match('/[a-z0-9_-]*/i', $data)
...
?>
Так вы будете уверены, что информация чиста и никаких неожиданностей не будет.
Если вы составляете список запрещённых символов, всегда можете просмотреть какой-нибудь нехороший %00 и подобные, о которых, скорее всего, не догадываетесь.
Разумеется, есть ситуации, когда нужна фильтрация, например, когда пользователь пишет комментарий.
Тогда надо отсекать плохие символы.
Но в принципе стараться надо валидировать.
Есть несколько команд, с которыми надо обращаться очень осторожно.
Это include, require, readfile, eval, ``, system, exec, create_function, dir, fopen и подобные.
Всегда посмотрите трижды, когда используете их, если в них используются данные, которые могут прийти от пользователя, будьте уверены — кто-то обязательно этим воспользуется.
<?php
include($_GET['module'] . '.php');
?>
Этот кусок опасен. Если злоумышленник введёт '../../../../../etc/passwd%00', будет рад, а вы — вряд-ли.
Аутентификация
Не забывайте, что cookies редактируются ни чуть не сложнее, чем то, что видно в адресной строке.
Поэтому всё, что приходит как печенье, потенциально — атака.
Так что не надо хранить в cookies уровень доступа пользователя или его ID.
Лучше всего дать PHP самому разбираться с этим, используя сессии.
<?php
session_start();
$_SESSION['userid'] = 168;
session_write_close();
?>
Кстати, в cookies вообще хранить что-либо надо очень скромно и три раза подумать, а надо ли?
Вывод
Всё время думайте о данных в переменных $_GET, $_POST, $_COOKIE, как об атаке злоумышленника.
Trust no one! :)