PHP: Хранение сессий в защищённых куках
- backend перестаёт помещаться на одном сервере и требуется хранилище сессий, общее для всех backend-серверов
- по различным причинам перестаёт устраивать скорость работы встроенных файловых сессий
Традиционно в таких случаях для хранения пользовательских сессий начинают использовать Redis, Memcached или какое-то другое внешнее хранилище. Как следствие возникает бремя эксплуатации базы данных, которая при этом не должна быть единой точкой отказа или бутылочным горлышком в системе.
Однако, есть альтернатива этому подходу. Возможно безопасно и надёжно хранить данные сессии в браузерной куке у самого пользователя, если заверить данные сессии криптографической подписью. Если вдобавок к этому данные ещё и зашифровать, то тогда содержимое сессии не будет доступно пользователю. Главное достоинство этого способа хранения в том, что он не требует централизованной базы данных для сессий со всеми вытекающими из этого плюсами в виде надёжности, скорости и масштабирования.
Описание механизма
Эта идея не нова и реализована во множестве фрэймворков и библиотек для различных языков программирования. Вот пара примеров:
- Python/Django
- Ruby/Ruby on Rails
Стоит заметить, в Ruby on Rails делают большую ставку на производительность этого механизма в сравнении со всеми остальными методами хранения сессий и используют его по умолчанию.
Большинство имеющихся реализаций работают следующим образом: записывают в какую-то куку строку, содержащую время истечения сессии, данные сессии и HMAC-подпись времени истечения и данных. При запросе клиента кука читается соответствующим обработчиком, затем проверяется подпись и сравнивается текущее время с временем истечения сессии. Если всё совпадает, обработчик возвращает данные сессии в приложение.
Однако, шифрование куки в распространённых реализациях этого механизма отсутствует.
Сравнение с классическим подходом
В итоге, хранение сессий в куках имеет следующие достоинства:
- Возрастает производительность веб-приложения, так как небольшая криптографическая операция дешевле сеанса сетевого обмена или доступа к диску для извлечения данных сессии.
- Возрастает надёжность веб-приложения, так как оно не зависит от внешнего KV-хранилища. Даже если хранилище сессий обеспечено средствами отказоустойчивости, это не наделяет его абсолютной стабильностью: переключение требует времени, а часть проблем (такие как ухудшение сетевой связности между регионами) и вовсе неискоренимы. Зачастую же сессии и вовсе хранятся на единственном сервере, являющимся единой точкой отказа всего веб-приложения.
- Экономия ресурсов. Не нужно больше хранить сессии, а значит от этого выиграют и владельцы маленьких сайтов, у которых сократится дисковая активность, и освободят несколько серверов владельцы крупных веб-проектов.
Имеются и недостатки, куда же без них:
- Возрастает объём данных, передаваемый клиентом
- Имеется ограничение на размер данных в сессии, связанное с ограничениями на размер кук. Обычно это чуть меньше 4 КБ кодированных данных.
- Клиент может откатить состояние сессии на любое выданное и подписанное ранее значение, криптоподпись которого ещё действительна в текущий момент времени.
Реализации для PHP
Когда я попытался отыскать что-то похожее для PHP, я с удивлением обнаружил, что не существует ни одной библиотеки, которая дотягивает до минимума требований:
- Безопасность: отсутствие ошибок при использовании криптографии
- Актуальная кодовая база: поддержка современных версий PHP, отсутствие deprecated-расширений в зависимостях (таких как mcrypt)
- Наличие тестов: сессии — это один из фундаментальных механизмов, и в основе реального приложения нельзя использовать незрелый код
Кроме этого считаю вовсе не лишним:
- Возможность шифрования: открытое хранилище сессии на клиенте, читаемое клиентом, не всем подходит.
- Максимально компактное представление данных — ради минимизации оверхеда и запаса ёмкости сессии
- Встраиваемость через SessionHandlerInterface
Реализации, которые я рассмотрел:
Репозиторий | Комментарий |
---|---|
github.com/Coercive/Cookie | Фактически не библиотека для работы с сессиями вовсе. Ставит шифрованную куку, не подписывая её. |
github.com/stevencorona/SessionHandlerCookie | Ближе всего к требованиям, но всё же имеет значительные недостатки:
|
github.com/mapkyca/Encrypted-Client-Side-Sessions |
|
Также я смотрел реализацию хранения сессий в куках в фрэймворке Slim версии 2.x, но там нет ни подписи, ни шифрования. О чём авторы сразу и предупреждают.
Почему важна проверка подписи и шифрования вместо подписи недостаточно? Во-первых, есть заметная вероятность, что кука с мусором расшифруется в какую-то сессию, особенно запись сессии короткая. Во-вторых, строка с сессией подвергается десериализации, а на вход десериализатора нельзя подавать строки из недоверенных источников.
После всех поисков я решил реализовать такую библиотеку самостоятельно.
Собственная реализация
Packagist: packagist.org/packages/snawoot/php-storageless-sessions
Github: github.com/Snawoot/php-storageless-sessions
Установка из composer:
composer require snawoot/php-storageless-sessions
Ключевые особенности:
- Обязательное шифрование. Алгоритм и режим — любой симметричный шифр на выбор, доступный в OpenSSL. По умолчанию: AES-256-CTR.
- HMAC-подпись куки любым хэш-алгоритмом на выбор из ассортимента криптографического расширения Hash. Он же используется для генерации производных ключей шифрования. По умолчанию: SHA-256.
- Реализованы контрмеры против атак по времени
- Помимо основного набора данных и времени истечения, подписью охвачен и ID сессии, что оставляет простор для связывания данных сессии с внешними данными.
- Реализация представлена в виде класса, совместимого с SessionHandlerInterface, а значит её можно прозрачно использовать практически с любыми PHP-приложениями.
- Минимальный оверхед хранения, привносимый шифрованием и подписью.
Пара слов о выборе режима шифрования. При использовании блочных режимов шифрования (ECB, CBC) длина шифротекста незначительно возрастает. Это связано с тем, что длина исходного сообщения должна быть кратна размеру блока. Из-за обязательного паддинга прирост длины составляет от одного байта до размера блока шифра. То есть для AES — от 1 до 16 байт. При использовании потоковых режимов шифрования (OFB, CFB, CTR, …) исходное сообщение не пропускается через блочный шифр, вместо этого блочный шифр используется для образования гамма-последовательности, и тогда длина шифротекста точно соответствует длине исходного сообщения, что лучше подходит для описываемой задачи.
Примеры использования
Небольшой скрипт, иллюстрирующий работу с этим хэндлером:
$value)
$_SESSION[$key] = $value;
echo "Updated session:";
} else
echo "Current session data:\n";
var_dump($_SESSION);
Пронаблюдать его работу, задавая разные значения сессии в строке запроса, можно по адресу: https://vm-0.com/sess.php.
Пример интеграции в Symfony:
framework:
session:
handler_id: session.handler.cookie
services:
session.handler.cookie:
class: VladislavYarmak\StoragelessSession\CryptoCookieSessionHandler
public: true
arguments: ['reallylongsecretplease']
В качестве реального демо я подключил этот хэндлер сессий к первому пришедшему на ум веб-приложению, которое использует сессии. Им оказалось DokuWiki: wiki.vm-0.com. На сайте работает регистрация и логин, а работу сессий можно наблюдать в куках.
Благодарю за внимание и надеюсь, что эта статья поможет развитию ваших проектов.
Комментарии (6)
4 апреля 2017 в 13:20 (комментарий был изменён)
–2↑
↓
Хоспаде… первое апреля же закончилось?4 апреля 2017 в 13:20
–1↑
↓
Мне тут кажется имеется множество уязвимостей.Например злоумышленник (З) может сам зарегистрироваться на сайте и будет примерно представлять содержимое шифрованной куки предположительно он может подобрать ключ к своей куки. Далее имея ключ и текст он может вычислить ключ для подписи.
Бороться этим можно только достаточно часто менять ключи. Что приведет к коллизиям у клиента кука подписана одним ключом, а на сервере он уже сменен. значит надо постоянно слать новые куки, что прям будут напрягать интернет. + Мы не уходим от узкого горлышка нам нужно хранить ключи для каждого пользователя.
Ну как то так.
4 апреля 2017 в 13:29
0↑
↓
На момент написания этого комментария алгоритм AES достаточно защищен от такой атаки.
4 апреля 2017 в 13:22
0↑
↓
Я понимаю, конечно, что много всего держать в сессии плохо, но как работать с ограничением 4КБ на 1 куку?
Нет ли смысла в данном случае предусмотреть в том числе сохранение данных в SessionStorage?4 апреля 2017 в 13:26
0↑
↓
То, что вы придумали обычно реализуют через 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) весьма применима и для вашего случая.
4 апреля 2017 в 13:27
+1↑
↓
Возможно безопасно и надёжно хранить данные сессии в браузерной куке у самого пользователя, если заверить данные сессии криптографической подписью.
… только до тех пор, пока «данные сессии» — маленькие. В противном случае вы получаете гигантский оверхед на каждом запросе.