Pull to refresh

Comments 107

UFO just landed and posted this here

На момент написания этого комментария алгоритм AES достаточно защищен от такой атаки.

Я понимаю, конечно, что много всего держать в сессии плохо, но как работать с ограничением 4КБ на 1 куку?
Нет ли смысла в данном случае предусмотреть в том числе сохранение данных в SessionStorage?

Нет ли смысла в данном случае предусмотреть в том числе сохранение данных в SessionStorage?
Нет, автор статьи сделал серверное решение, оно абсолютно прозрачно для клиента. Использование интерфейса Storage предполагает какую-то дополнительную логику на стороне клиента, а это из другой оперы.

То, что вы придумали обычно реализуют через JWT, для чего есть множество библиотек, в том числе на PHP.
А ещё есть статьи где популярно объясняется почему реализация сессий на клиенте это плохая идея в общем и с использованием JWT в частности:
https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-bad-standard-that-everyone-should-avoid
http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/


Ваша реализация, судя по тому что я прочел по диагонали, идеологически похожа, так что вторая статья (и её follow-up) весьма применима и для вашего случая.

Отмечу, что перечисленные недостатки относятся исключительно к использованию JWT в качестве хранилища сессионных данных.

Вы правда читали статью по второй ссылке?


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

Да, читал. В обоих статьях большая часть — бред и паника на пустом месте.

Тогда будьте добры, уточните что во второй статье относится исключительно к JWT и не релевантно данной статье.

Данной статье оно как раз очень даже релевантно. Оно не релевантно для использования JWT в тех задачах, для которых JWT создавался.

Читаем вторую статью. «The drawbacks:
  • They take up more space
  • You cannot invalidate individual JWT tokens
  • Data goes stale
  • Implementations are less battle-tested or non-existent»

Каждый пункт как-то отражен в данной статье, автор про это задумывался. Так что корректно было бы обсуждать каждый момент по существу, а не просто кидать ссылку.

Ссылку я кинул для того, чтобы не повторять то, что кто-то уже структурировал в виде статьи. Задумываться над этим мало, нужно иметь решение к вопросам. Но исходя из их сущности решения скорее всего быть не может в общем случае. Если считаете иначе — давайте обсуждать.


Опять таки, чтобы не повторяться, рекомендую прочесть саркастический ответ на часто встречаемые контр-аргументы (он упомянут в шапке той статьи, если пропустили): http://cryto.net/~joepie91/blog/2016/06/19/stop-using-jwt-for-sessions-part-2-why-your-solution-doesnt-work/

В статьях, на которые вы ссылались, какие-то люди пытаются попиариться на относительно модном JWT. А реальность такова, что есть pros и cons у JWT-like и server-side сессий. У обоих есть фунадаментальные достоинства и недостатки и есть дефекты реализаций/спецификаций.
Надо стремиться использовать наиболее подходящее решение в каждом случае и исправлять дефекты, но заявлять «JWT sucks», как авторы тех статей — неправильно.

Голословно — не правильно. А если аргументировано, указывая на фундаментальные ошибки и заблуждения, то вполне себе можно и нужно.

JWT или другой способ шифрованного хранения сессионных данных


В JWT, кстати, данные хранятся в открытом виде.

Вы Abstract читали (первый параграф)?:


JSON Web Token (JWT) is a compact, URL-safe means of representing
claims to be transferred between two parties. The claims in a JWT
are encoded as a JSON object that is used as the payload of a JSON
Web Signature (JWS) structure or as the plaintext of a JSON Web
Encryption (JWE) structure, enabling the claims to be digitally
signed or integrity protected with a Message Authentication Code
(MAC) and/or encrypted.

Весьма однозначно сказано что содержимое вполне себе может быть зашифрованным.

Так, как на хабре минусовать самого себя? :-D

Я всегда думал, что JWE это самостоятельный тип токена, отличный от JWT, пардон.
The contents of the JOSE Header describe the cryptographic operations applied to the JWT Claims Set. If the JOSE Header is for a JWS, the JWT is represented as a JWS and the claims are digitally signed or MACed, with the JWT Claims Set being the JWS Payload. If the JOSE Header is for a JWE, the JWT is represented as a JWE and the claims are encrypted, with the JWT Claims Set being the plaintext encrypted by the JWE. A JWT may be enclosed in another JWE or JWS structure to create a Nested JWT, enabling nested signing and encryption to be performed.
https://tools.ietf.org/html/rfc7519#page-6
Первая статья — типа «Давайте не будем использовать HTTP — вон в нём сколько дыр уже нашли». Какой может быть ответ? Нет, спасибо, мы дырки-то позатыкаем, но использовать не бросим.
Возможно безопасно и надёжно хранить данные сессии в браузерной куке у самого пользователя, если заверить данные сессии криптографической подписью.

… только до тех пор, пока "данные сессии" — маленькие. В противном случае вы получаете гигантский оверхед на каждом запросе.

Как решается вопрос принудительной инвалидации сессии?


А вообще, по-моему, данный подход не является сессией в обычном смысле слова в веб-разработке. Сессия — хранение каких-то данных на стороне сервера такое, что результаты запросов клиента зависят от истории предыдущих запросов. Тут же каждый запрос получается самодостаточным, что-то вроде WebForms.

UFO just landed and posted this here

Да, но ViewState работало в дополнение к сессиям, а не вместо них...

Может запамятовал, но вроде они отправляли с фронта на сервер всё состояние сессии, ну и получали его от сервера.

Они отправляли на сервер все состояние страницы. Сессия работала отдельно.

Они отправляли состояние страницы, а не сессии. Если у пользователя открыты сразу две вкладки — эти понятия принципиально различаются.


Кроме того, ViewState не сохраняется при переходах между страницами.

Может. Я особо не разбирался, как фротендер с ними только немного работал больше 10 лет назад.

А он никак не решается. Разве что костылем в виде хранилища отозванных сессий.

В обычных сессиях решается удалением файла/записи сессии.

Добрый день!

Как и указано в недостатках,
Клиент может откатить состояние сессии на любое выданное и подписанное ранее значение, криптоподпись которого ещё действительна в текущий момент времени.
Однако здесь важно заметить: если, к примеру, стоит практическая задача разлогинить юзера, то инвалидация одной сессии сама по себе ничего не даёт. Нужно прервать все сессии. И здесь у классического подхода к хранению сессий тоже возникают проблемы, так как в таком случае нужно найти и удалить все сессии, которые принадлежат пользователю. То есть это значит нужно хранить и поддерживать соответствие пользователя списку сессий. Дальше — больше: если используются sticky-сессии, то тогда один сервер должен пошариться по множеству других, чтобы разлогинить юзера через удаление сессий.

Удаление сессий в любом случае непрактичный подход. Вместо этого в сессию записывают факт логина вместе со временем логина. А в профиле пользователя держат время последнего логаута. Если время логина сессии раньше времени последнего логаута — считать её неактивной. При таком подходе для логаута нужно всего лишь обновить одно значение в записи о пользователе.
А в профиле пользователя держат время последнего логаута.

И как, бишь, вы это время между разными серверами будете синхронизировать в случае sticky sessions?


(и как вы будете решать задачу "разлогинь меня в этой сессии, не трогая остальные"?)

И как, бишь, вы это время между разными серверами будете синхронизировать в случае sticky sessions?
Время логаута не нужно синхронизировать, оно в профиле пользователя
(и как вы будете решать задачу «разлогинь меня в этой сессии, не трогая остальные»?)
Если нужно предоставить пользователю возможность разлогинить любую сессию, на которую он покажет пальчиком, то и при том и том подходе нужно хранить список сессий. Если только ту, в которой пользователь прямо сейчас сидит, то у классического подхода тут преимущество. Обязательно пользуйтесь классическим подходом, если Ваша бизнес-задача — разлогинивать текущие сессии пользователей.
Время логаута не нужно синхронизировать, оно в профиле пользователя

… который хранится где?


Если нужно предоставить пользователю возможность разлогинить любую сессию, на которую он покажет пальчиком, то и при том и том подходе нужно хранить список сессий.

После чего совершенно не понятно, зачем гонять данные сессии на клиента.

… который хранится где?
Который хранится в базе, а не в сессии.
После чего совершенно не понятно, зачем гонять данные сессии на клиента.
В таком случае заведите ишью в репозиториях джанги и рельсов, потребуйте немедленно удалить непонятные бэкенды для сессий.
Который хранится в базе, а не в сессии.

… которая становится той же самой единой точкой отказа. В чем разница-то?

База это база, а хранилище сессий — это хранилище сессий. Предложенный хандлер устраняет SPOF в виде хранилища сессий.

Так что мешает хранить сессии в той же БД, что и профили? Как была одна точка отказа, так и осталась.

Вы когда-либо хранили сессии в БД?
Отлично. Интересно узнать ответы на два вопроса:
  1. Как вы удаляли старые сессии?
  2. Каков ваш план масштабирования пропускной способности базы на запись при росте числа сессий?
Напоминаю при этом, что подход с хранением сессий в куках сам по себе не полагается на базу, если приложение не использует базу вовсе, то и для хранения сессий она не нужна. Если вдруг стоит требование инвалидировать сессии, то решение возможно чтениями из базы, что, согласитесь, совсем другое дело.
Как вы удаляли старые сессии?

Регулярной задачей.


Каков ваш план масштабирования пропускной способности базы на запись при росте числа сессий?

Шардинг.


Напоминаю при этом, что подход с хранением сессий в куках сам по себе не полагается на базу, если приложение не использует базу вовсе, то и для хранения сессий она не нужна.

… напоминаю, что подход с хранением сессий в куках работает только при малом объеме данных в сессии.

Автор статьи спросил бы у вас: Допустим, memcache упал/втупил/стал недоступен. У вас или лежит сайт, или все незалогинены.


От себя замечу, что если вам хотя бы чуть-чуть важны залогиненые пользователи, под сессии лучше использовать redis.
Там есть и сохранение на диск, и реплики, и кластер.

Ведь в Memcached тоже есть реплики, кластер? А Redis ещё и имеет ряд недостатков для сессии, на пример session locking https://github.com/phpredis/phpredis/issues/37
Я не использовал Redis в highload пректе и не могу лично от себя ничего сказать…

Да, в memcache прикрутили что-то для репликации и можно использовать несколько серверов memcached в качестве кластера.
Но данные при перезагрузке ноды вы все равно потеряете.
В highload redis отлично используется для хранения сессий, к нему обращаются php, python, go.


По вашей ссылке это проблема не redis, а конкретного php extension.
Там в обсуждении люди описали, как сделать свой handler с session locking.
Я не программист, но лично мне кажется, что session locking нужно делать в приложении, а не внутри extension, так как это специфичная фича — каждый может видеть её реализацию по разному.

UFO just landed and posted this here
UFO just landed and posted this here
На самом деле на ваш вопрос «зачем» есть ответ в самом начале статьи:

На некоторой стадии развития веб-проекта возникает одна из следующих ситуаций:

backend перестаёт помещаться на одном сервере и требуется хранилище сессий, общее для всех backend-серверов
по различным причинам перестаёт устраивать скорость работы встроенных файловых сессий

Традиционно в таких случаях для хранения пользовательских сессий начинают использовать Redis, Memcached или какое-то другое внешнее хранилище. Как следствие возникает бремя эксплуатации базы данных, которая при этом не должна быть единой точкой отказа или бутылочным горлышком в системе.


Я вот никогда раньше и не задумывался, что можно так сессии хранить. Теперь обдумываю…

Замечу, что большие куки — это плохо. Несколько раз у разных подсистем (не php) ловили падения при большом объеме кук. Ловить такие ошибки на проде — всегда очень занимательная задача.

Так и не понял ради чего всё это сделано? Если хотите работать с куками, то пожалуйста, зачем тогда вообще дергать сессию (а уж шифровать или нет это уже зависит от нужд). Кроме того не обязательно дергать диск или сетевые протоколы для сессий, можно хранение и в памяти сделать (если так критично к скорости чтения сессий). Кроме того, если у вас SSD, то у скорость чтения с диска довольно большая.

Для того, чтобы перенести хранение сессий на сторону клиента без изменения механизма работы с ними.


В памяти хранить сложно, если у вас несколько инстансов апп-сервера — нужен механизм репликации (причём мастер-мастер) памяти между серверами.

in memory nosql base
И все таки проще и логичнее просто перейти на работу с куками без сессий. С учетом того, что во многих фрейморках почти всега используются классы обертки для работы с сессиями. Просто в них использовать данные из куков. Но как пример работы через сессий через куки — довольно хороший)

Начало поста:


Традиционно в таких случаях для хранения пользовательских сессий начинают использовать Redis, Memcached или какое-то другое внешнее хранилище. Как следствие возникает бремя эксплуатации базы данных, которая при этом не должна быть единой точкой отказа или бутылочным горлышком в системе.

Однако, есть альтернатива этому подходу...

Не факт, что проще и логичней, не очень хорошо и в классы-обёртки сессий фреймворков лезть, и переписывать код, если $_SESSION используется.

Синхронизация между устройствам? Вот вы зашли на сайт с рабочего компа, затем в транспорте с планшета (еще и с 3G), что будет?

Ну, это болезнь любых сессий вообще, а не только данной реализации.

UFO just landed and posted this here

Если раздать ее на все клиенты и таким образом засинхронить — это будет уже не сессия, а профиль пользователя.

Спасибо за статью идея интересная но есть пару проблемных моментов.

1. HMAC — это не электронная подпись. Поскольку строится на базе симметричного ключа и соответственно может быть подделана одной стороной без возможности это опровергнуть другой стороной. Классическая ЭП на ассиметричной криптографии этого не допускает, поскольку ЭП строится с помощью закрытого ключа который есть только у подписывающей стороны.

2. В приведенных вами исходниках ключ шифрования вшит в код, а с учетом того, то шифрование должно осуществляться на стороне клиента этот ключ может быть перехвачен третьей стороной и после чего данные будут подделаны.

Пример. Вася подключается в Web-клиенту Банка. К нему грузятся скрипты, содержащие ключ шифрования.
Вася отключается, но ключ у него есть. Вася знает что у Зины на счету много денег. Он от имени Зины строит сессию (ему даже авторизовываться не надо) и совершает перевод.

3. Вы скорее всего скажите, что сессия будет защищена TLS. Но раз так, тогда смысла в доп. шифровании нет.

Для исправления ситуации необходимо как минимум для каждой сессии получать рандомный ключ шифрования. Но даже это не защитит от атак повтором. Поэтому нужно капать в сторону использования ассиметричной крипты.

Вы пост-то вообще читали? Сторона тут только одна — сервер, который сначала генерирует куку, а потом ее же читает.

Что вы называете сессией?

Если только некий id, тогда непонятно причем тут ограничения на 4кб в куку, если же в сессии присутствует состояние полное состояние клиента, то для того чтобы его нехранить на сервере его надо шифровать и оно должно шифроваться на клиенте.

В статье схемы взаимодействия не хватает.

Веб-сессия пользователя, оно же сеанс — это понятие, объеденяющее активность пользователя по взаимодействию с веб-ресурсом с момента первого обращения к ресурсу и до прекращения взаимодействия ресурсом.


Обычно под сессией понимают термин "состояние сеанса", означающий блок нетипизированных данных, хранимых где-нибудь в течении всего сеанса, и к которым имеет доступ только серверный код.


Клиентскому же коду доступ к сессии не нужен — у него есть localStorage для тех же целей.

Я, например, генерировал id сессии хешем от юзерагента, ip, даты и секретного ключа.
И не сохранял в куках, а просто генерировал при каждом запросе.

Я не понимаю. Где вы храните дату создания сессии, если id сессии генерируется на ее основе?

UFO just landed and posted this here

Отлично. Теперь любые два пользователя, которые сидят с одного IP и одного браузера в один день имеют общую сессию!


Вы что за сайты-то делаете, если ваша целевая аудитория, по всей видимости, бездетные незамужние люди, не сидящие за NATом?..

Браузер обновился — поменялся хэш. Надо брать не весь юзер агент, а из него платформу + ip + time zone,

Согласен. Просто в моем случае лучше чтоб "в любой непонятной ситуации разлогинивался". Но сам метод можно модифицировать под конкретную ситуацию.

Добрый день!

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

Во-первых, IV это не публичный ключ, и хранить его отдельно от сообщения нет смысла.
Во-вторых, нет подписи куки, и я писал в статье, почему она должна быть.
Дело хозяйское.

1. Имеет, инвалидация же.
2. Шифрование же.
Вы свято верите, что кука в браузере протухнет по истечении времени и тогда данные инвалидируются. Однако, клиент сам управляет куками, они ему подконтрольны полностью. Вот в том числе поэтому нужно время экспайра и его подпись. Но не только поэтому, как я уже говорил.
Почитайте статью и гляньте код хотя бы прежде чем делать утверждения. Вектор используется для инвалидации без центрального хранилища.
Я почитал ваш код и утверждаю: куки подконтрольны клиенту, какие он проставит, такие и будут. Если клиент решит их хранить вечно — в вашем случае сессия будет длиться вечно.
Идентификатор — да, данные за ним — нет. Они не передаются клиенту, как и у файловой сессии, у которой есть собственный GC. Я, например, использую xcache с ненулевым временем жизни и вектор для ключа. Промах приводит к регенерации, никаких «вечно».
UFO just landed and posted this here
можно, МИТМом, локальным прокси например, если очень надо
UFO just landed and posted this here
Вышеуказанным методом. Например, org.littleshoot.proxy… Или что Вы хотели спросить?
UFO just landed and posted this here

И что? Клиент-то ее получил и прочитал, значит, и обратно послать может. Все эти флажки "безопасности" — они для добронамеренных клиентов.

UFO just landed and posted this here
Куки с флагом HttpOnly не видны браузерному коду

Это, простите, как? А отправляет их кто?


Куки с флагом Secure пересылается только через HTTPS (HTTP с использованием SSL — Secure Socket Level)

Только если клиент выполняет это требование. И да, даже если клиент выполняет это требование, он может не выполняеть требование про валидацию сертификатов, или сертификаты могут быть подменены на уровне устройства и так далее, далее, далее.

UFO just landed and posted this here

Так вот, клиенты браузерами не ограничиваются. Поэтому если у нас есть способ куку перехватить (а если его нет, то все обсуждения, в общем-то, на пустом месте), то мы всегда можем ее отправить и сделать подмену сессии.

UFO just landed and posted this here
Куки для API?

Нет, для подконтрольного агента.


Даже если куки для API, то они защищены ssl, а значит расшифровать перехваченный трафик вы всеравно не можете

Еще раз говорю: если у вас есть способ передавать куки с гарантированной невозможностью перехвата, то вы можете их использовать для чего угодно и как угодно. Инвалидация, подписание и прочий цирк нужен, когда такого способа нет по тем или иным причинам.

Инвалидация нужна не только для защиты от компрометации кук и прочих токенов, но и для защиты от пользователя, который перестал быть доверенным.

Для этого надо пользователя инвалидировать целиком.

Пользователя инвалидировать не проблема, проблема инвалидировать все его открытые сессии, если принято решение на каждый запрос не проверять не запрещён ли с момента последнего запроса вход пользователю.

Если принято такое решение, то придется сначала подумать, как вообще определить "все его открытые сессии". И так далее. Каскад решений, каждое из которых влияет на финальную функциональность.


Опять-таки, есть разница между инвалидацией куки и инвалидацией сессии.

Основная и ключевая проблема на мой взгляд — инвалидация данных сессии.
Да, единая точка отказа и бутылочное горлышко в виде редиса/мемкшеа есть, но уже существует множество решений ждя больших объемов данных
Автор пишет, что у него были проблемы с поиском текущих реализаций данного механизма на PHP.

Справедливости ради, стоит отметить, что в Laravel один из драйверов сессий, которые идут с фреймворком по умолчанию, предназначен именно для хранения данных сессии в куках.

https://laravel.com/docs/master/session
Посмотрел реализацию в ларавеле:
  1. Громоздкая сериализация: ключи в жсон прям текстом пишутся, от всех полей в base64 паддинг ("===") остаётся, могли бы трансляцию в urlsafe base64 сделать, подпись прям в строковом виде. Можно было бы снизить расходы этой части раза в полтора точно.
  2. Экспайр не охвачен подписью, то есть сессии — всегда вечные.

В остальном всё очень хорошо сделано.
С академической точки зрения интересно. Но если подумать, что такое сессия? Это некий ключ к состоянию. Хранить состояние на клиенте? Ну да, можно, если это простая корзина. Корзину даже шифровать не надо. user_id хранить даже в зашифрованном виде я бы не рискнул.

Состояние может быть очень сложным. Дополнительные сложности в отладке ради экономии 1 запроса, но платить за это жирнющей кукой — сомнительная выгода.

Сессия — не ключ к состоянию, а само состояние. Хранение состояния сессии на клиенте — ключ к созданию стейтлесс серверов, например в целях балансировки нагрузки и отказоустойчивости.

Хранение состояния сессии на клиенте — ключ к созданию стейтлесс серверов, например в целях балансировки нагрузки и отказоустойчивости.

… один из возможных.

Тут ещё в личку вопрос поступила просьба рассмотреть вот этот модуль: https://github.com/psr7-sessions/storageless

На момент написания статьи я уже был с ним знаком, но не включил его в обзор по той причине, что он является не хандлером сессий, а middleware для сообщений PSR-7 (по сути аналог фильтра в UNIX-конвейере). То есть подходит не кому угодно, а тем, у кого всё приложение выстроено как обработчик сообщений PSR-7.

Если по этому критерию он подходит, то вот особенности данной реализации. Не вчитывался слишком вдумчиво, так как не работал с PSR-7, но обнаружил:
  • Нет шифрования.
  • Используется формат вебтокена JWT. Вместе с этим в качестве сериализатора всегда используется не один из встроенных сериализаторов сессий PHP, а всегда json_encode. Строго говоря, он может сериализовать не всё то же самое, что может сериализовать каждый из внутренних сериализаторов сессий PHP, нужно это учитывать. Кроме того, сама по себе кука получается размером ещё больше по сравнению со всеми остальными реализациями — оверхед максимальный.
  • Исходя из использования JWT, помимо HMAC-подписи доступна ещё RSA-подпись. Это, наверное, плюс, но я не вижу юзкейса: подписывает и проверяет одна и та же сторона, она же имеет оба ключа. В таком случае намного оптимальнее отказаться от асимметричной криптографии для подписи в пользу HMAC — и по соображениям простоты, и по соображениям скорости.


Про всё это (плюс про то, что я писал в статье) и так в документации написано.

Одно место я так и не понял: https://github.com/psr7-sessions/storageless/blob/master/src/Storageless/Http/SessionMiddleware.php#L305-L311 — не знаю, означает ли это захардкоженный лимит времени жизни куки (и сессии) или нет.
Sign up to leave a comment.

Articles