Занимаясь разработкой веб приложений всё время приходится сталкиваться с различными
проблемами безопасности. В данной статье собран некоторый мой опыт по безопасной
работе с веб-формами и немножко кукисами - конечно это только малая часть общей
защиты веб приложений, но ей обычно не уделяют достаточно времени при разработке.
Итак наша задача будет разработать безопасную систему авторизации пользователей
и безопасную форму для отправки сообщений. В первом приближении таким веб-приложением
является обычный форум. Давайте сразу определимся мы будем разрабатывать безопасный форум - в котором
некоторые удобства пользователей будет ущемленны в пользу безопасности. В частности
пользователь должен обладать современным браузером с подержкой JavaScript и
CSS. Но тем не меннее если не совршать ничего криминального данное приложение
будет достаточно удобно в использовании.
Наверняка все знакомы с таким классическим способом защиты как блэк листы
- когда пользователь по IP адресу или имени заносится с стоп лист за соверщение
каких то неправомерных действий. Однако это вам не поможет есть атакующий будет
пользоваться какими-то автоматическими программами для флуда (со множества
ip адресов и ников) или брута. Или же иногдда невозможно забанить пользователя
в силу каких-то причин - например вы ожидаете важного сообщения от него. А
ведь противостоять таким атакам гораздо проще чем кажется.
Часть 1. Защищаемся от флуда.
1.1 Случайные формы
Итак рассмотрим для начала какие карты есть у нас на руках - у нас есть имя пользователя
- или какие-то данные (session id) по которому мы сможем судить что это именно
тот пользователь которому разрешенно пользоваться формой для отправки даных (не
зарегестрированным пользовтаелям надо есстесвенно запретить оставлять комментарии),
так же у нас есть форма, которую надо заполнить и отправить на сервер. Пускай
она выглядит как-то так:
<FORM METHOD=POST ACTION="post.pl">
<INPUT TYPE="hidden" name="thread" value="123">
<INPUT TYPE="hidden" name="whateveryouwant" value="someval">
Subject:
<INPUT TYPE="text" NAME="subj"><BR>
<TEXTAREA NAME="text" ROWS="10" COLS="40"></TEXTAREA>
<br>
<INPUT TYPE="submit"> < /FORM>
Итак первое что мы можем сделать что бы запутать злобного хаккира это добавить
в форму элемент случайности - таким образом атакующий будет вынужден каждый раз
анализировать полученную страничку, что бы получить правильное значение.
Что же можно использовать в качестве такого элемента случайности ? Нам ведь
известно сколько комментариев оставил на форуме пользователь (в общем случае
можно считать другие активные действия пользователя) - соответвенно сделаем
привязку к этому: <FORM METHOD=POST ACTION="post.pl">
<INPUT TYPE="hidden" name="thread" value="123">
<INPUT TYPE="hidden" name="whateveryouwant" value="someval">
<INPUT TYPE="hidden" name="post_number" value="_">
Subject:
<INPUT TYPE="text" NAME="subj"><BR>
<TEXTAREA NAME="text" ROWS="10" COLS="40"></TEXTAREA>
<br>
<INPUT TYPE="submit"> < /FORM>
я оставил значение post_number в форме не заполненным - это и есть наша динамическая
величина, всякий раз выдавая форму пользователю мы присваеваем
post_number = количество постов пользователя + 1
потом получаяя форму в серверной части скрипта производим простое сравнение
и получаем результат - если значение post_number не совпадает с ожидаемым -
значит
нужно перенаправить пользователя. Этот простой способ может использоваться
и для получения сообщений от незарегестрированных пользователей - тогда в качестве
элемента случайности можно ввести номер сообщения в цепочке (те параметр last_message
= число сообщений в треде + 1) - но тут надо учитывать корректировку на то
что
иногда бывают жаркие обсуждения (в случае форума) и значение "число сообщений
в треде" может увеличиться за тов ремя пока пользователь пишет сообщение.
Естественно такое средство обломает достаточное количество флудеров но всё
таки флудер для такой формы написать легко - достаточно понять зависимости
формы от количества сообщений. Поэтому надо вносить гораздо более случайный
элемент...вернее всю форму сделать случайной ! Для этого нам потребуется совсем
не много !!! Давайте рассмотрим профиль пользователя в общем виде:
user_id |
user_name |
user_password |
1 |
admin |
*** |
Давайте немножко изменим профиль пользователя. Внесём туда такие поля:
user_id |
user_name |
user_password |
u_thread_name |
u_whateveryouwant_name |
1 |
admin |
*** |
|
|
Итак мы добавляем 2 случайных элемента - u_thread_name и u_whateveryouwant_name
- они будут хранить случайные значения имени поля и значения для элемента thread
и whateveryouwant нашей формы. Те реально мы сгенерируем случайные имена для
этой формы ! Те наша форма будет выглядеть так: <FORM METHOD=POST ACTION="post.pl">
<INPUT TYPE="hidden" name="A$dm8fi2k35leokE" value="123">
<INPUT TYPE="hidden" name="Ykli3fvRcd#kpQ" value="someval">
....
А профиль пользователя таким образом:
user_id |
user_name |
user_password |
u_thread_name |
u_whateveryouwant_name |
1 |
admin |
*** |
A$dm8fi2k35leokE |
Ykli3fvRcd#kpQ |
теперь когда вы получаете данные от пользователя вы можете сравнить значение
в u_thread_name и в полученной форме - если они не совпадают значит либо пользователь
пишет из старой формы, либо вас атакуют. И в том и в другом случае не стоит
добавлять данные в базу а переслать правильную (вновь сгенерированную - с новыми
случайными значениями) форму - пользователь в таком случае нажмёт лишний раз
Post, а атакующий просто будет гонять трафик. Есственно что бы ещё больше запутать - генерируйте несколько (случайных)
ненужных полей - назначение которых просто запутать человека, анализующего
html код. Правда что бы он не знал какие поля лишние - желательно сохранить
в профиле пользователя общее число полей в форме - а потом сравнить с полученными
данными.
1.2 Шифруемся !
Давайте представим что атакующий смог написать программу, которая анализирует
Html код и выдёргивает нужные поля (это действительно просто!) а после этого
флудит. Нам нужно ещё более случайный элемент и ещё более запутать анализатор
кода.
Многие разработчики часто забывают о том что современные браузеры обладают
поистине огромными возможностями в плане отображения информации - вы можете
создавать скрытые элементы, невидимые пользователю...а давайте размножим нашу
форму...
<FORM METHOD=POST ACTION="post.pl">
<INPUT TYPE="hidden" name="A$dm8fi2k35leokE" value="123">
<INPUT TYPE="hidden" name="Ykli3fvRcd#kpQ" value="someval">
.... < FORM METHOD=POST ACTION="post.pl">
<INPUT TYPE="hidden" name="jfopefmiwef#$rf" value="124">
<INPUT TYPE="hidden" name="Sko9dgj23niu%" value="someval">
....
< FORM METHOD=POST ACTION="post.pl">
<INPUT TYPE="hidden" name="Kjiwl[1%7ksa" value="125">
<INPUT TYPE="hidden" name="Ljiwp25jnjfp0" value="someval">
....
а что бы пользователь не увидел этого чуда мысли применим таблицу стилей:
< style>
.hform1 { visibility:hidden; show:hide;position:absolute; left:0px; top:0px; z-index:99; } < /style>
< FORM class=hform1 METHOD=POST ACTION="post.pl">
<INPUT TYPE="hidden" name="A$dm8fi2k35leokE" value="123">
<INPUT TYPE="hidden" name="Ykli3fvRcd#kpQ" value="someval">
.... < FORM class=hforml METHOD=POST ACTION="post.pl">
<INPUT TYPE="hidden" name="jfopefmiwef#$rf" value="124">
<INPUT TYPE="hidden" name="Sko9dgj23niu%" value="someval">
....
< FORM class=hform1 METHOD=POST ACTION="post.pl">
<INPUT TYPE="hidden" name="Kjiwl[1%7ksa" value="125">
<INPUT TYPE="hidden" name="Ljiwp25jnjfp0" value="someval">
....
Таким образом мы получаем то что первая и последняя формы станут невидимы пользователю
- а для анализатора html кода они будут присутвовать в тексте. Таким образом
автору предпологаемого флудера мы ставим ещё одну задачу - надо будет помимо
html форм анализировать ещё и таблицу стилей. Естественно что имена стилей и
их содержание так же должно формироваться динамически дабы не облегчать задачу.
1.3 JavaScript
Конечно атакующий может сообразить что видима должна быть только одна форма и
искать её в тексте по значению class отличающиеся от других. Однако и тут мы
можем подставить ему подножку ;) Думаю все веб разработчики знакомы с JavaScript
- полезнейшая штука... что же она поможет и нам =).
Во первых вы можете изначально приминив ко всем формам один стиль можете
по таймеру (5 сек) изменить его у одной из форм - пользователь такого не заметит,
а вот программа... Вообще меня поражает как разработчики пренебрегают возможностями
JavaScript для защиты своих веб-приложений... Можно привести миллион советов
как обезопасить себя от флуда с помощью JavaScript - например придавать значения
ключевым полям формы с помощью скрипта на реагирующего на onsubmit...там же
мы можем дополнять или удалять определённые поля у формы. Кроме того не стоит
забывать что в JavaScript существует замечательная функция eval, позволяющая
на лету формировать скрипты: <SCRIPT LANGUAGE="JavaScript"> < !--
var suxx = "alert (\"holly shit, it works !!!\");"
....
// do some shit
....
eval (suxx);
//--> < /SCRIPT>
Эта функция открывает нам дорогу в шифрованные скирпты - например можно защифровать
основное тело скрипта, выполняющее различные манипуляции с формой простым методом
подстановки. Не забывайте что адресовать формы можно не только по имени но и
через массив document.forms, что даёт нам определённые преимущества при проектировании
полиморфных скриптов.
Таким образом что бы написать флудер для форм, формирующихся из защифрованного
скрипта на JavaScript с полиморфным расшифровщиком вам потребуется написать
собственный отладчик скриптов, что уже согласитесь довольно непростое занятие,
которое подсилу далеко не многим. Хотя и написать такую защиту сможет отлько
довольно квалифицированный специалист, коими многие веб-мастера имеющие в своём
запасе только один-два учебника по php и mysql конечно же не являются. 1.4 More JavaScript
Многие почему то так же забывают о такой замечательной вещи как document.write
- А ведь через него можно и чудеса творить =) в частности преобразовать такой
html - код:
<BODY> < HR> < HR> < B>Say</B> <I>hello</I> <U>fucking</U> world ! < HR> < HR> < /BODY>
В такой:
<BODY>
<SCRIPT LANGUAGE="JavaScript">
<!--
document.write ("\u003c\u0048\u0052\u003e\u003c\u0048\u0052\u003e\u003c\u0042\u003e\u0053\u0061\u0079\u003c\u002f\u0042\u003e\u0020\u003c\u0049\u003e\u0068\u0065\u006c\u006c\u006f\u003c\u002f\u0049\u003e\u0020\u003c\u0055\u003e\u0066\u0075\u0063\u006b\u0069\u006e\u0067\u003c\u002f\u0055\u003e\u0020\u0077\u006f\u0072\u006c\u0064\u0020\u0021\u003c\u0048\u0052\u003e\u003c\u0048\u0052\u003e");
//-->
</SCRIPT>
</BODY> Довольно простой ход, который однако оставит малолетних читателей журнала
хаккир за бортом...
1.5 At the end
На последок хочет сказать что методы описанные здесь остановят большинство флудерских
инструментов. И если вы затрудняетесь реализовать всё описанное выше поробуйте
начать с малого - следите хотя бы за количеством постов ;)
Кроме того обязательно почитайте информацию о полиморфизме и шифровании.
В приминении к JavaScript я такого не видел - но последние 10 лет вирусмейкеры
трудились не зря - поняв общие конценции вам будет легко сделать собственный
скрипт... Ссылки найдёте в конце статьи. Часть 2. Защищаемся от брута.
Атака с применением грубой силы довольно серьёзная вещь, если ей пренебрегать
- конечно есть такие жесткие методы, как блокировать пользователя на опреелённый
промежуток если он ввёл неправильно пароль n-ое число раз. Однако такие методы
не всегда применимы, кроме того атакующий может избрать своей мишенью пользователя
и просто не давать ему залогиниться на страничку... а это согласитесьнехорошо,
особенно когда вам несут важную весть ;)
2.1 Измените ваше представление о мире.
Итак где же искать вызод ? Мы могли бы применить случайные элементы в формах,
но ведь нам несчем их связывать, ведь пользователь ещё не вошёл в систему...
А в чём проблема ? Давйте взглянем на классическую форму для ввода имени пользователя
и пароля:
< FORM METHOD=POST ACTION="login.pl">
Login: <INPUT TYPE="text" NAME="user"><BR>
Password: <INPUT TYPE="password" NAME="password"><BR>
<INPUT TYPE="submit"> < /FORM>
А что если разделить процесс ввода имени пользователя и пароля ? что бы пользователь
сначала представился, а потом ввёл пароль... Те мы получаем на странице входа
простую форму, которую не надо брутить:
< FORM METHOD=POST ACTION="login.pl">
Login: <INPUT TYPE="text" NAME="user"><BR>
<INPUT TYPE="submit"> < /FORM>
Однако введя login пользователь перенаправляется на страничку для ввода пароля:
< FORM METHOD=POST ACTION="login.pl">
<INPUT TYPE="hidden" NAME="session" VALUE="1234567890468"><BR>
Password: <INPUT TYPE="password" NAME="password"><BR>
<INPUT TYPE="submit"> < /FORM>
Стоп. а ведь страничку для ввода пароля мы можем формировать случайным образом
используемым для защиты от флуда, как было описанно выше.
2.2 Следите за сессиями
В предыдущей форме было введенно поле сессии session - это неспроста ;) Моё мнение
что сессии надо создавать не только для успешно залогившихся пользователей, но
и для тех кто только пытается. Заведите простую табличку:
session_id |
user_id |
session_status |
Соответвено помимо session_id (и конечно же user_id, полученного на основании
введённого имени пользователя) задайте поле которое характеризует статус процесса.
Например WAIT_FOR_PASSWORD, PROCESS_LOGIN и WHAT_THE_FUCK_IS_GOING_ON:
-
WAIT_FOR_PASSWORD - пользователь ввёл имя и должен ввести пароль
-
PROCESS_LOGIN - пользователь ввёл пароль - не обязательное состояние в
которое попадает сессия когда пользователь уже ввёл данные, но скрипту необходимо
выполнить ещё какие-то проверки, прежде чем пользватель будет считаться вошедшим
-
WHAT_THE_FUCK_IS_GOING_ON - ошибка или имени пользователя или пароля -
такая сессия сразу же становится недействительной и атакующий должен сначала
начать новую сессию - а она начинается с ввода имени пользователя.
Таким образом программе брутуфорсу придётся атаковать 2 формы (одну простую -
и одну довольно сложную - если она сделана в сответвии с нашими требованиям к
защите от флуда) - а на такое способна далеко не каждая программа !
Часть 3. XSS - atalavista, baby !
В последнее время кругом только и говорят об XSS атаках. Суть атаки заключается
в возможности вставки своего скрипта в страницу. Нет ничего проще. XSS атакой
может быть как и безобидное alert ("hello lamaz") так и сложный скрипт для
запуска трояна. Однако мы будем рассматривать несколько другой аспект - возможность
XSS
атаки с целью похищения данных о пользователе - его cookie файла.
Почему то многие разработчики препочитают хранить в куках именно логин с
паролем. Немногим лучшим представляется и случайное значение сессии, которое
просто связанно с именем пользователя. Конечно наиболее безопасным решением является использование сессии, однако
грамотные сессии помогут избежать атак связанных с возможностями увода куков.
3.1 IP адрес в куках
Действительно почему бы не сохранять помимо сессии в куках ещё и IP пользователя
??? Решение логичное (особенно если в серверной части чётко прослеивается соответсвие
правильная сессия = с правильного IP адреса) и имеет право на жизнь.
Таким образом даже если злоумышленник украдёт значение сессии из куков он
не сможет им воспользоваться - тк:
-
сессия это случайное число, не имеющее ничего общего с логином/паролем
-
подделать IP адрес так что бы с него было возможно вести полноценный обмен
данными в современном мире довольно тяжело, особенно в глобальной сети
3.2 Хэши ...
Однако не стоит забывать о том что пользователь и злоумышленник могут оказаться
за одним прокси или vpn сервером. В таком случае кража сессии обернётся печальным
образом. Ну и кроме того мы делаем защищённую систему, которая не должна кидаться
во все стороны сообщениями о своих пользователях. Тут нам приходит на помощь
функции хэширования.
Что представляет собой фунция хэширования ? Это функция которая принимает
на вход строку любой длины, и выдаёт результат фиксированной длинны. Запонмите
- фиксированная длинна - это очень важно ! В частности это дает возможность
теоретическому существованию коллизий (когда в результате хэширования 2-х разных
строк получается одинаковый результат). Кроме того использовав хэшированное
значение IP адреса злоумышленник может вычислить хэш фукнкцию для всех возможных
значений IP диапазонов - соответвенно произойдёт утечка информации о пользователе.
Выход опять же есть не в простом сохранении IP в куках или сохранении его
хэша, а в приминении более сложной функции, например:
h (session xor h (IP xor session))
где h - это функция хэширования, session - это уникальный идентификатор сессии,
а IP соответвенно IP адрес пользователя... Таким образом сохраняя в таблице сессий
только значение session мы можем вычислить с правильного ли IP пришла эта сессия.
Не вздумайте тольков таблице сессий хранить вычисленное значение этой функции
!!! Это повысит быстродействия, но сведёт на нет все наши пляскис бубном вокруг
IP - тк вы не сможете его проверить.
3.3 А зачем вся эта возня с сохранением в тайне IP адреса ?
А действительно зачем ? Кому это надо ? Однако если вы разрабатываете безопасную
систему то вы должны поставить и безопасность её пользователей на высокий уровень.
Давайте на минуту отвлечёмся от сабжа и заимёмся промывкой мозгов ;)
Давайте представим что мы находимся на хакерском форуме, каждый из посетитеелй
которого хочет сохранить в тайне свои данные (давайте оставим разговоры - я
сижу через vpn мне пох - каждый из нас достаточно наследил в сети по молодости
и незнанию). В то же самое время владелец форума в жажде поисеть больше посетителей/накрутить
бабла/сдать всю эту шайку в интерпол ставит на страницу кучу счётчиков, ведёт
логи на сервере и кроме того к каждому посту у него на форуме прилеплен ип
адрес владельца. А теперь давайте представим что будет если база с такого сервера попадёт
в руки компетентных органов...неприятно правда ? Хотя зачем им база - некоторые
андеграунд ресурсы и так сообщают им данные о своих посетителях - например
форум undeground information center - uinc.ru с радость расскажет вам кого
интересует информация о андеграунде. Ну и конечно же множество сайтов имеют
незапароленную статистику - webalizer например введите server.com/wstat/ и
посмотрите кто посещал этот ресурс...
Но самым главным порождением зла являются эти мелкие счётчики - mail.ru/hotlog.ru/etc.ru
которые день за днем ведут логи в том числе и закрытых форумов (если они у
вас установленны) - для того что бы быть в курсе всего происходящего с вами
за день в инете надо просто порыться в базе хотлога например на предмет вашего
ипа - всё станетн а свои места - какие сайты вы посещаете, где сколько времени
проводите...etc ... и эти базы ведутся годами ;(
Итак если мы разрабатываем безопасное веб приложение оно должно обеспечивать
безопасность и всех его пользователей. Возьмите это за правило - никгда
не собирайте прямые логи с данными о посетителях - если вам это так интересно
используйти хэш функции для сохранения адресов в базе. Паранойя ? Желание
создать безопасные для пользователей приложения - вам ведь будет неприятно
если вас поймают из-за того что по модолости вы однажды светанули реальными
данными...
Часть 4. Но и это ещё не всё...
4.1 Игры разума - почему иногда следует думать логически...
Намутить можно ещё много чего чем пренебрегают пользоваться веб разработчики
- например логика ;) Например используя промежуточную страницу с сообщением о
том что данные обрабатываются можно добится неплохой защиты от флуда. Например
процесс постинга сообщания выглядит так (общий случай - в некоторых скриптах
4 и 5 объеденены):
1. Пользователь запращивает страницу с темой - GET запрос на сервер
||
\/ |
2. Сервер выдаёт страницу с топиком - Пользователь пишет сообщение
||
\/ |
3. Пользователь отсылает сообщение - POST запрос на сервер
||
\/ |
4. Сервер выдаёт страницу по типу: Ваше сообщение принято подождите 5
с
||
\/ |
5. Пользователь запрашивает страницу с темой - GET запрос на сервер
||
\/ |
Учтите при этом что например thread_id во всех этих запросах одинаков!!!
В алгоритме флуда присутвует только пункт 3. Теперь смотриме давайте построим
маленькую логическую цепочку.
Что бы пользователь мог запостить ему надо сначала запросить страницу (мы
отказались от возможности сохранения страницы на жесткий диск в части 1 - когда
обсуждали борьбу со флудом - элементы случайности в формах) ! Он не может
сразу воспользоваться готовой формой, ему надо запросить страницу с темой !
Введите в профиле пользователя CURRENT_STATE - который отражает текущее состояние
пользователя. Например можно использовать такие значения:
-
ALLOW_POST - значит последний запрос был GET, причём именно страницы с
темой - состояние 1-2 на предыдущей картинке
-
POST_PROCESS - значит последний запрос был POST и пользователю показывают
картинку - подождите 5 сек - состояния 3-4
-
ALL_OTHERS - другие состояния - например пользователь запросил страницу
поиска, изменяет свой профиль - всё прочее
Подумайте ведь что бы создать новую тему пользователь должен сделать некоторые
простые действия - в частности он должен запросить сначала индексную страницу
раздела в котором собирается создавать тему, потом он должен запросить страницу
где находится форма для создания темы - и лишь потом он создаёт тему. Храните
состояние пользователя ! Ещё раз перерисуем таблицу состояния:
0. Пользователь находится изначально в любом состоянии
||
\/ |
1. Пользователь запращивает страницу с темой - GET запрос на сервер -
состояние стало ALLOW_POST
||
\/ |
2. Сервер выдаёт страницу с топиком - Пользователь пишет сообщение -
ALLOW_POST
||
\/ |
3. Пользователь отсылает сообщение - POST запрос на сервер - состояние
стало POST_PROCESS
||
\/ |
4. Сервер выдаёт страницу по типу: Ваше сообщение принято подождите 5
с - POST_PROCESS
||
\/ |
5. Пользователь запрашивает страницу с темой - GET запрос на сервер -
состояние стало ALLOW_POST
||
\/ |
Таким образом пропишем простые правила для каждого состояния:
-
из ALLOW_POST пользователь может перейти в состояния ALLOW_POST, POST_PROCESS,
ALL_OTHERS - и при этом запостить данные из формы !!
-
из POST_PROCESS - пользователь может перейти в ALLOW_POST или ALL_OTHERS,
но не может постить данные или быть в состоянии POST_PROCESS
-
из ALL_OTHERS пользователь может перейти только в ALLOW_POST или ALL_OTHERS
Учтите что при переходе из состояния в состояние следует сохранять thread_id
- так что бы он был одинаковым в цепочке ALLOW_POST - POST_PROCESS - ALLOW_POST.
4.2 Минимизируем потери на трафике и скорости работы
Как ни странно даже уловив возможность флуда или брута веб приложения продолжают
генерировать странички со сложным контентом, занимающие сотни килобайт трафика
и драгоценные проценты работы процессорного времени. А зачем ? Флудеру или
брутеру пофиг какой вы крутой дизайнер...
Обнаружив атаку - например несовпадение CURRENT_STATE из части 4.1 или несовпадение
случайных элементов в форме из пунктов 1 и 2 - не надо генерировать сложную
страницу - верните код 302 и новый Location - это займёт немного трафика
и времени, а так же снизит нагрузку на процессор.
В идеале я бы вообще возвращал 404, тк некоторые программы в этом
случае останавливаются =))) но мы не можем быть уверенны что предусмотрели
все ситуации и хитрый пользователь (не злоумышленник !) как то случайно не
собъет нашу систему (например сохранит страницу на hdd и попробует через месяц
сделать пост с неё) - что бы этот процесс произошёл прозрачно для пользователя
мы и укажем код 302 и тот же самый Url - тогда только на следующий запрос,
который не сделает программа атакующего, вернётся правильная страница с нужными
формами и пользователю будет присвоен правильный статус.
Часть 5. Outro
... |