Как можно взломать свой же Web проект?

ysscbjfgwqabbxcj9lrhbo228x4.jpeg


Недавно мне позвонили и попросили помочь разобраться с очень странными симптомами на сайте. А симптомы выглядели вот так. Cовершенно разные клиенты звонили и рассказывали, что они на сайте попадают в личные кабинеты других случайных клиентов. Логинятся под собой. Входят в свой аккаунт, а потом бац — и уже в аккаунте другого клиента. Правда они не догадывались, что кто-то другой так же мог оказаться в их личном кабинете.


Давайте я расскажу «историю неуспеха», чтобы другие на эти грабли не наступали.


Я попробую как в детективе приоткрывать детали произошедшего. Особо смышленые догадаются в середине рассказа где закралась проблема.


В проекте имеется аутентификация пользователей. Используется классическая схема со случайно сгенерированным ID сессии, который устанавливается в Cookie клиентского браузера. На сервере сессионные данные хранятся в memcached, т.к. Application серверов несколько.
Очевидно, клиенты друг друга не взламывали, пароль у них у всех один и тот же внезапно не установился. На первый взгляд какая-то сложность случилась с сессиями пользователей — они начали попадать в сессии друг друга. Но как? Раз пока непонятно, то можно попробовать рестартануть memcached, чтобы все сессии сбросились. Да, не особо гуманно — пользователям придется перелогиниться. Но рестарт memcached проблему не решил.


Стали копать логи и действительно видно, что, например, некий определенный клиент всегда заходил в одного и того же IP адреса, а сейчас вдруг его действия логируются с кучи разных IP. Еще, что более важно, в логах именно по аутентификации (когда клиент вводит логин и пароль) видно, что данный клиент логинился только со своего привычного IP адреса. Делаем вывод — когда клиент становится другим клиентом, это происходит не через аутентификацию, а он конкретно попадает в сессию другого клиента (facepalm).


Код меняли? Нет, не релизили ничего. Сервера перенастраивали? Ну…. только включили в Nginx кеширование картинок, которые отдавались не как статика (статика давно кешируется), а генерируются на лету. Но при этом не меняются. Т.е. есть endpoint вида: /path/image? id=NNNN По сути это практически как статика, т.к. картинка с определенным id никогда не изменится. Поэтому в целях оптимизации ресурсов Application серверов решили такие запросы тоже закешировать.


Раз я про это написал, то это важно. И т.к. больше ничего другого не меняли, то практически очевидно, что проблема в этом кешировании. Кеширование конечно отключили, но проблема не ушла.


Так как же подобное кеширование устроило чехарду на сайте?


Вот еще несколько вводных (подробностей реализации).


  • В проекте реализован подход «автосоздания сессий». Т.е. если приходит клиент, у которого нет куки, в которой хранится ID сессии, то Backend при ЛЮБОМ запросе в ответ сгенерирует ID сессии и в заголовках ответа будет присутствовать Set-Cookie, устанавливающий сессионную куку. То же самое происходит при запросах к /path/image? id=NNNN
  • Картинки отдаются в том числе неавторизованным клиентам.


Вот тут уже можно догадаться, что же произошло…


А произошло следующее. Пришел неавторизованные клиент без сессионной куки. Ушел GET запрос на картинку /path/image? id=NNNN, который в ответ сгенерировал ID сессии и,»рвем на себе волосы», закешировался! Да, закешировался вместе с заголовком Set-Cookie устанавливающим куку с определенным ID сессии. Дальше приходят другие пострадавшие, получают эту же картинку вместе с ОДНИМ И ТЕМ ЖЕ ID сессии, которая у них принудительно переписывается в браузере. В итоге мы имеем кучу пользователей, у которых в браузерах в куках один и тот же ID сессии.


Получается, что клиент авторизуется — сессия его. Потом другой клиент авторизуется и теперь в этой сессии у нас второй клиент. Первому достаточно обновить страничку и он уже в сеансе второго клиента. А теперь представьте сумашествие, которое наблюдалось, когда куча клиентов «жили» в одной сессии.


Ну вы понимаете, что одна из целей вирусов — это украсть куки с ID сессий, чтобы злоумышленник мог удаленно подменить у меня сессию и работать от имени пострадавшего. А тут и красть ничего не нужно — ID сессии у всех один. Сайт перешел в «однопользовательский» режим.


Да, конечно, нужно еще привязывать сессию к IP адресу, User Agent«у и т.п., тогда бы такой чехарды не произошло. Но уж чего не было, того не было сделано.


Но на этом история не заканчивается. Пытливый читатель должен был пойти почитать документацию Nginx про кеширование и найти вот это:
http://nginx.org/ru/docs/http/ngx_http_proxy_module.html#proxy_cache_valid
Ответ, в заголовке которого есть поле «Set-Cookie», не будет кэшироваться.


Ага, почему в данном случае Set-Cookie закешировалось? Тут еще один нюанс проекта: движок в каждом ответе делает Set-Cookie для других нужд, не касающихся ID сессии. В том числе когда отдает картинку. Поэтому, чтобы кеширование все-таки заработало, в конфиге Nginx присутствовал еще такой параметр:
http://nginx.org/ru/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers
proxy_ignore_headers Cache-Control Expires Set-Cookie;


Вот из-за этого кука и закешировалась.


Чтобы рассказ был поучительным необходимо рассказать о том, как нужно было сделать правильно с точки зрения настроек Nginx. Конкретно в данном случае, добавление
http://nginx.org/ru/docs/http/ngx_http_proxy_module.html#proxy_hide_header
proxy_hide_header Set-Cookie;


должно решить проблему.


P.S. На чем написан Backend? Мне кажется не имеет значения. «Автосоздание сессиий» может присутствовать на любой платформе. Иногда это полезно и упрощает жизнь, а иногда опасно. Думайте головой и все будет хорошо. Рассказ скорее про особенности настройки Nginx.


P.P. S. Это не история успеха. Это история набивания шишек. Разработчики проекта в курсе слабых мест в архитектуре, которые привели к описанному инциденту. Поэтому большая просьба в комментариях не развивать тему того, какие разработчики и админы тупые, а вы с 20 годами опыта все бы предвидели и не наступили на эти грабли.

© Habrahabr.ru