вакцина для сайта БОРИС ВОЛЬФСОН Хакер, номер #075, стр. 034 BORISVOLFSON@GMAIL.COM HTTP://SPLENDOT.COM СОЗДАНИЕ МНОГОУРОВНЕВОЙ ЗАЩИТЫ ОТ ВЗЛОМА ВЕБ-ПРИЛОЖЕНИЙ «МОЙ САЙТ – МОЯ КРЕПОСТЬ» - ТАКИМ ДОЛЖЕН БЫТЬ ДЕВИЗ ВСЕХ ВЕБ-РАЗРАБОТЧИКОВ. В ЭТОЙ СТАТЬЕ Я РАССКАЖУ, КАК КАМЕНЬ ЗА КАМНЕМ И КИРПИЧ ЗА КИРПИЧОМ ПОСТРОИТЬ ТАКУЮ КРЕПОСТЬ. МЫ НАЧНЕМ С ЗАЩИТЫ НА СТОРОНЕ КЛИЕНТА И ЗАКОНЧИМ ОБОРОНОЙ СЕРВЕРНОЙ ЧАСТИ [три круга обороны на стороне клиента.] Начнем с самого простого – с отпугивания «юных хакеров» ;). Итак, малолетнее создание открыло браузер, зашло на твой сайт, нашло форму и пытается своими коварными ручками ввести туда что-нибудь нехорошее. Прежде всего, воспользуемся возможностями языка HTML. Для начала выберем подходящий элемент управления, например, если надо выбрать «место жительства», то безопасней будет использовать выпадающий список (а не строку ввода). Что касается последней, то желательно выбрать и максимальную длину для строки ввода. Второй круг обороны будет работать на JavaScript, который поможет нам подвергнуть строгому аудиту все, что пользователю вздумается ввести в наши формы. Мы можем, например, убедиться, что введен действительно e-mail, а не SQL-запрос, который выдаст злоумышленнику пароли всех пользователей. Третий и самый последний круг исповедует идеологию AJAX, поэтому не является чисто клиентским. После заполнения очередного поля мы сделаем запрос на сервер о правильности введенных данных. Рассмотрим плюсы и минусы данного подхода. Он, безусловно, отпугнет взломщиков-дилетантов, которые не умеют пользоваться ничем кроме браузера. Отмечу также еще один приятный эффект – улучшение юзабилити сайта, ведь при неправильном заполнении формы пользователь еще до ее отправки получит соответствующее сообщение. Минус у защиты на стороне клиента только один – обойти ее проще пареной репы, так что сохраняем страницу у себя на сервере и отключаем в браузере JavaScript. А еще лучше - работаем не через браузер, а с помощью скрипта. Так что защиту на стороне клиента можно считать рвом вокруг нашей крепости. А поскольку ров – это самое-самое начало, то приступим к возведению стен =). [защищаемся на сервере.] Сразу оговорюсь, какие аспекты я освещу в следующей части статьи. Здесь ты не услышишь ни о тонких настройках безопасности в Линукс, ни о том, как правильно собрать защищенный апач и какие модули к нему прикрутить. Не будет произнесено ни слова о настройках PHP (любимые мои magic quotes). Оставим эти дела сисадминам, - они не зря свой хлеб едят, а сами займемся программированием. Бросим беглый взгляд на схему, по которой нам предстоит работать дальше: [проверка входных данных.] Для начала определим, откуда наш скрипт может получать потенциально опасную информацию. Первое, что приходит на ум – это формы, которые заполняет пользователь. Данные из них могут передаваться двумя методами. При использовании метода POST данные поступают скрипту на стандартный вход, а если применяется метод GET, то информация передается в адресной строке: листинг Язык: URL http://www.example.ru/index.php?variable=value Ты любишь печенье? Нет? А вот взломщики любят, ведь часть информации передается именно через куки. Обычно это идентификатор сессии и другая информация, которая сохраняется во время серфинга юзера по сайту. Для скрипта, написанного на PHP, все вышеописанные методы получения внешней информации практически равнозначны, поэтому ты можешь использовать массив $_REQUEST (правда, это не очень безопасное решение). Вроде бы все, но нет! Подумай, откуда еще скрипт получает данные? Конечно, из БД (или из редких нынче текстовых файлов)! Возникает закономерный вопрос: «А почему мы не можем доверять информации, полученной из базы данных?». Есть два источника угрозы. Во-первых, взломщик мог получить доступ к базе данных и занести туда произвольную информацию, например, с помощью SQL-инъекции. Во-вторых, есть такая нехорошая вещь, как XSS – межсайтовый скриптинг. В данном случае, злоумышленник мог положить в базу данных информацию, используя наши же скрипты. Например, он может написать в гостевой книге и в комментарии несложный JavaScript, который передаст ему куки всех пользователей, его прочитавших. Поэтому фильтры проверки надо накладывать на данные и при их поступлении от пользователя, и при выдаче этих данных самому пользователю. [типы проверок.] Откуда могут поступить вредоносные строчки, мы с тобой выяснили, теперь рассмотрим, что в них может быть злого и как с этим бороться. Начнем с самого простого – ограничим длину вводимых пользователем данных. Такая банальная проверка может понадобиться для работы с базами данных. Также следует проверить все поля для введения этих данных, если заранее известен их тип (в частности, на входе мы можем получить число или дату и осуществить необходимую проверку). В PHP можно использовать функцию is_numeric для проверки «на число», правда, с осторожностью – в разных версиях результаты ее работы будут немного отличаться. В общем случае удобно использовать регулярные выражения, которые удачно реализованы во многих языках или библиотеках. Для проверки на корректность адреса электронной почты можно использовать такой regexp: листинг Язык: PHP eregi("^[A-Z0-9._%-]+@[A-Z0-9._%-]+\.[A-Z]{2,6}$", $email) Что касается проверки входных данных на принадлежность к миру чисел, то регулярные выражения могут помочь и здесь. Давай, кстати, немного усложним задачу: входной параметр должен быть целым числом без знака. Для этого сначала уберем пробельные символы в начале и в конце строки, а затем проверим строку регулярным выражением. листинг Язык: PHP function is_unsigned_integer($val) { $val=str_replace(" ","",trim($val)); return eregi("^([0-9])+$",$val); } Регулярное выражение «^([0-9])+$» (без кавычек) означает, что строка должна содержать только цифры и их количество должно быть не меньше одной. Аналогичные регулярные выражения необходимо использовать и для остальных данных. Лично я люблю использовать класс Validate из библиотеки PEAR, который позволяет проводить достаточно сложные проверки в одну строчку: листинг Язык: PHP $validate = &new Validate(); $validate->string( $username, array('format'=>VALIDATE_ALPHA . VALIDATE_NUM . VALIDATE_SPACE ) ) $validate->email( $email ) $validate->number( $age, array( 'min'=>0, 'max'=>100 ) ) Методы класса Validate возвращают false, если проверка не пройдена. Сначала мы проверяем имя пользователя: оно должно состоять только из букв, цифр и пробелов. Затем мы поверяем адрес электронной почты, и, наконец, убеждаемся, что возраст пользователя – это число в диапазоне от 0 до 100. [экранируем спецсимволы.] Спецсимволы нас, прежде всего, не устраивают в случае записи информации в базу данных, поскольку в них наверняка кроется великое зло SQL-инъекций и XSS-атак. PHP предлагает нам целый арсенал средств борьбы с подобного рода уязвимостями. Все, что от нас требуется – это использовать их. Приведу список функций, которые следует использовать для экранирования: string addslashes ( string str ) – добавляет к символам апострофа, кавычкам, обратному слешу и нулевому байту слеш. string quotemeta ( string str ) – добавляет слеш к символам . \ + * ? [ ^ ] ( $ ). string htmlspecialchars ( string string ) – переводит в HTML-сущности амперсанд, кавычки, апостроф, знаки «больше» и «меньше». string htmlentities ( string string ) – аналог htmlspecialchars, но использует HTML-сущности (энтитисы). string mysql_real_escape_string ( string unescaped_string ) – экранирует строку для использования в mysql_query. Похоже, настало время рассказать про magic_quotes, о которых я торжественно обещал молчать в начале главы. Эта опция автоматически экранирует данные из массивов $_GET, $_POST, $_COOKIE. Значит, при написании кода нам надо это учитывать, чтобы не добавить два раза обратные слеши: листинг Язык: PHP function escape_smart($value) { if (get_magic_quotes_gpc()) { $value = stripslashes($value); } $value = mysql_real_escape_string($value); return $value; } [BBcode и wiki.] Иногда все-таки необходимо дать пользователю возможность форматировать введенный им текст, но разрешать напрямую вводить теги слишком опасно. Самым простым выходом будет использование какой-либо разметки, например BBcode или wiki. В таком случае при создании входных данных пользователь использует ограниченный язык разметки, который затем конвертируется в HTML. Пользователю мы дадим возможность выделять шрифт полужирным и курсивом: листинг Язык: BBcode [b]Здесь будет полужирный текст, [i]а здесь - полужирный курсив[/i][/b], a это обычный текст. Дальше надо просто заменить теги на их HTML-аналоги. Каждый тег надо обрабатывать отдельно, а не просто сменить квадратные скобки на угловые. Тогда на выходе мы получим: листинг Язык: HTML <strong>Здесь будет полужирный текст, <em>а здесь - полужирный курсив</em></strong>, a это обычный текст. Такой подход позволяет довольно просто избежать угрозы XSS-инъекций, да и реализуется он без особых напрягов. Другой вариант (менее безопасный) – разрешить пользователю вводить только определенные теги, а остальные отсеивать. Тот же пример будет теперь выглядеть так: листинг Язык: BBcode $s = strip_tags($s, "<em><strong>"); Хочу сразу предупредить, что подобный вариант - не панацея: некоторые форумы, использующие BBcode, были успешно взломаны при помощи XSS. [база данных: специфика.] Есть несколько советов, которые можно дать тем, кто активно использует базы данных. Во-первых, можно использовать препарированные запросы, которые поддерживаются большинством библиотек для работы с базами данных. На Перле создание и исполнение такого запроса выглядит примерно так: листинг Язык: Perl $st = $db->prepare("SELECT user_name FROM users WHERE id = ?;"); $st->execute($email); Первая строка формирует запрос и посылает его на сервер, где он компилируется. Обрати внимание на тот факт, что параметр id заменен знаком вопроса и будет подставлен при исполнении. Вторая строка просто передает параметр для запроса, который уже не парсится сиквелом, а просто считается данными. Совет номер два будет касаться хранимых процедур и триггеров. На данный момент все современные серверы баз данных поддерживают такую функциональность (MySQL начиная с пятой версии). Напомню, что хранимая процедура – это скомпилированный запрос на сервере, которому при исполнении могут передаваться параметры. А триггер – это специальная хранимая процедура, которая выполняется при определенном событии, например, при удалении записи из БД. Использование этих механизмов не только увеличит производительность приложения, но и повысит его защищенность. [outro.] В этой статье я привел достаточно много способов защиты. Чтобы выбрать подходящий набор методов, необходимо тщательно подумать, насколько мощная система безопасности нужна тебе на сайте, иначе твои высокие стены не сможет преодолеть не только взломщик, но и обычный пользователь. ТИП: Текст НАЗВАНИЕ: СЛЕДОПЫТЫ ВЕРОЯТНОСТЬ СДЕЛАТЬ АБСОЛЮТНО НЕПРОБИВАЕМЫЙ САЙТ СТРЕМИТСЯ К НУЛЮ, ПОТОМУ ЧТО ПРОФЕССИОНАЛ ПРИ ДОСТАТОЧНОМ КОЛИЧЕСТВЕ ВРЕМЕНИ И ЖЕЛАНИЯ СМОЖЕТ НАЙТИ УЯЗВИМОСТИ ВЕЗДЕ, ТЕМ БОЛЕЕ ЕСЛИ РЕЧЬ ИДЕТ О КРУПНОЙ СИСТЕМЕ. ЧТОБЫ ОСТАНОВИТЬ ТАКОГО ПРОФЕССИОНАЛА, НЕОБХОДИМО ИСПОЛЬЗОВАТЬ СИСТЕМУ ЛОГОВ. ЛОГ – ЭТО ЖУРНАЛ, В КОТОРЫЙ ЗАПИСЫВАЕТСЯ ИНФОРМАЦИЯ О ДЕЙСТВИЯХ ПОЛЬЗОВАТЕЛЯ, НАПРИМЕР, СТРАНИЦЫ, КОТОРЫЕ ОН ПОСЕЩАЛ. ДЛЯ ЗАЩИТЫ САЙТА ПОНАДОБИТСЯ ВЕСТИ ЖУРНАЛЫ «ПЛОХИХ» ДЕЙСТВИЙ ПОЛЬЗОВАТЕЛЯ. ПРЕЖДЕ ВСЕГО, СЮДА ОТНОСИТСЯ МНОГОКРАТНОЕ НЕКОРРЕКТНОЕ ЗАПОЛНЕНИЕ ФОРМ. АЛГОРИТМ ДЕЙСТВИЙ МОЖЕТ БЫТЬ СЛЕДУЮЩИМ: В РЕЗУЛЬТАТЕ, ПРИ ОБЫЧНОЙ ОШИБКЕ ДАННЫЕ О ПОСЕТИТЕЛЕ ПРОСТО ЗАПИШУТСЯ В ЛОГ-ФАЙЛ, КОТОРЫЙ МОЖНО БУДЕТ ПРОСМОТРЕТЬ ПРИ НЕОБХОДИМОСТИ. ПРИ ЯВНОЙ ПОПЫТКЕ ВЗЛОМА АДМИНИСТРАТОРУ БУДЕТ НЕМЕДЛЕННО ОТПРАВЛЕНО ПИСЬМО НА ЭЛЕКТРОННЫЙ АДРЕС. ТЕПЕРЬ Я УТОЧНЮ ПОНЯТИЕ «ВРЕДОНОСНОГО КОДА» - ЭТО ПОПЫТКА SQL-ИНЪЕКЦИИ ИЛИ XSS-АТАКИ. ТО ЕСТЬ НАМ НАДО ФИЛЬТРОВАТЬ ВСЕ ПОПЫТКИ ВВЕСТИ ТЕГИ, JAVASCRIPT И ЛЮБЫЕ SQL-ОПЕРАТОРЫ. К ЭТОЙ ДРУЖНОЙ КОМПАНИИ ТАКЖЕ ДОБАВЛЯЮТСЯ СПЕЦСИМВОЛЫ, ВКЛЮЧАЯ НОЛЬ. ТИП: Текст НАЗВАНИЕ: ПРОВЕРКА ИСТОЧНИКА КОГДА Я РАССКАЗЫВАЛ О ЗАЩИТЕ НА КЛИЕНТСКОЙ СТОРОНЕ, Я ОГОВОРИЛСЯ, ЧТО СТРАНИЦУ МОЖНО СОХРАНИТЬ НА ДРУГОМ СЕРВЕРЕ ИЛИ ЛОКАЛЬНОЙ МАШИНЕ И ПОДПРАВИТЬ, КАК ТЕБЕ НРАВИТСЯ. НО ЭТУ БРЕШЬ МОЖНО НЕМНОГО ЗАЛАТАТЬ, ПРОВЕРЯЯ, ОТКУДА К НАМ ПРИШЕЛ ПОЛЬЗОВАТЕЛЬ. ДЕЛАЕТСЯ ЭТО ПРИ ПОМОЩИ ПЕРЕМЕННОЙ $_SERVER["HTTP_REFERER"], КОТОРАЯ КАК РАЗ И СОДЕРЖИТ ПРЕДЫДУЩУЮ СТРАНИЦУ, А НАМ ОСТАНЕТСЯ ТОЛЬКО ПРОВЕРИТЬ, ДЕЙСТВИТЕЛЬНО ЛИ ПОЛЬЗОВАТЕЛЬ ПРИШЕЛ ОТТУДА. ОДНАКО У ВЗЛОМЩИКА ОПЯТЬ ЖЕ ЕСТЬ ДВЕ ВОЗМОЖНОСТИ - ЛИБО ПРОСТО ИСПОЛЬЗОВАТЬ TELNET И С ЕГО ПОМОЩЬЮ СФОРМИРОВАТЬ ЗАПРОС, ЛИБО НАПИСАТЬ СКРИПТ И ИСПОЛЬЗОВАТЬ ЕГО, ВЕДЬ ЗНАЧЕНИЕ ПЕРЕМЕННОЙ $_SERVER["HTTP_REFERER"] ФОРМИРУЕТСЯ НА СТОРОНЕ КЛИЕНТА. СДЕЛАЕМ ВЫВОД: ТАКАЯ ЗАЩИТА СПОСОБНА ОТПУГНУТЬ ТОЛЬКО НЕПРОФЕССИОНАЛЬНОГО ВЗЛОМЩИКА, А ПРОФЕССИОНАЛУ ОНА ПРОСТО ДОСТАВИТ НЕБОЛЬШОЕ НЕУДОБСТВО. Политика безопасности для пользователей Взломщик может пытаться сломать не весь сайт, а только получить доступ к аккаунту отдельного пользователя. И тут встает вопрос социального характера. Попробуем решить его при помощи программирования ;). Для захвата аккаунта хакеру необходимо завладеть логином и паролем пользователя. Логин обычно находится во всеобщем доступе и служит идентификатором пользователя, а вот пароль, по идее, знает только сам пользователь. Теперь цель ясна – надо защитить пароль ;). [strong password.] Самым «тупым» методом взлома пароля является bruteforce, проще говоря, перебор. Обычно для перебора используют словари, либо просто перебирают всевозможные комбинации по заданной маске. Нашей первой задачей будет борьба с подбором. Для этого пароль должен быть достаточно длинным – более 6 символов и содержать разнообразные символы: цифры, строчные и прописные буквы. По мере необходимости длину пароля можно увеличить, а в необходимые символы добавить еще и знаки препинания. Маленьким примечанием к данному параграфу будет необходимость блокировать вход пользователя на сайт (минут, скажем, на 30) после 3-5 неудачных попыток ввода логина и пароля. В таком случае взломщику придется использовать прокси, которые при большой необходимости можно также блокировать. Для более мощной проверки пароля рекомендую использовать расширение CrackLib. Оно позволяет прогнать пароль по словарю и осуществить ряд дополнительных проверок: листинг Язык: PHP // Загружаем словарь $dictionary = crack_opendict('/usr/local/lib/pw_dict'); // Проверяем пароль $check = crack_check($dictionary, ‘Q6g$b87gHjn5_4t5sdf!23HLayi'); // Получаем результат проверки $res = crack_getlastmessage(); echo $res; // 'strong password' // Закрываем словарь crack_closedict($dictionary); Сюда же отнесу такую возможность, как смена паролей через определенный промежуток времени. Такая опция не даст взломщику возможность подбирать пароль брутфорсом в течение года. [автогенерация паролей.] Зачем мучить пользователей и заставлять их придумывать пароли длиной в 10 букв и цифр, когда проще это сделать программно? Заодно можно будет проверить пароль на уникальность, что еще больше повысит безопасность. [хеш от паролей.] Представь себе подобную картину – взломщик получил доступ на чтение базы данных пользователей твоего сайта… и никак не может воспользоваться этой информацией ;). Это не моя больная фантазия, а реальность, если вместо паролей ты хранишь их хеши. Хеш – это «уникальная» строка (или число), которая генерируется по паролю при помощи специальной функции, например md5. По хешу восстановить саму строку практически невозможно, поэтому этот способ хранения паролей очень надежен. [опасные действия.] Базу пользователей у нас уже крали, теперь предположим, что взломщик смог завладеть сессией пользователя, то есть работает под его логином. Ситуация типичная – можно просто подождать, пока человек выйдет из-за компа, если речь идет об офисе, или попробовать украсть куки с идентификатором сессии. Первое, что попытается сделать злой хакер, - это сменить пароль. Именно при подобных действиях надо повторно запрашивать пароль пользователя, ведь если взломана сессия, нам удастся предотвратить крупный ущерб. К опасным действиям стоит отнести: удаление аккаунта, крупные финансовые транзакции, массовую рассылку писем и прочее. [привязка сессии к IP.] Чтобы взломщику было сложней завладеть сессией, надо кроме идентификатора хранить также IP пользователя. Такой подход сделает невозможным работу с другого компьютера даже при имеющемся правильном идентификаторе сессии. ВКЛЮЧИ ОПЦИЮ MAGIC_QUOTES_GPC ДЛЯ АВТОМАТИЧЕСКОГО ЭКРАНИРОВАНИЯ ВСЕХ ВХОДЯЩИХ ДАННЫХ НИКОГДА НЕ ДОВЕРЯЙ НИКАКИМ ВНЕШНИМ ДАННЫМ ПРОСТАВЬ ОПЦИИ DISPLAY_ERRORS = OFF И LOG_ERRORS = ON ДЛЯ ОТКЛЮЧЕНИЯ ПОКАЗА ОШИБОК И ДЛЯ ВКЛЮЧЕНИЯ ИХ ЖУРНАЛИРОВАНИЯ |