На некоторой стадии развития веб-проекта возникает одна из следующих ситуаций:
- backend перестаёт помещаться на одном сервере и требуется хранилище сессий, общее для всех backend-серверов
- по различным причинам перестаёт устраивать скорость работы встроенных файловых сессий
Традиционно в таких случаях для хранения пользовательских сессий начинают использовать Redis, Memcached или какое-то другое внешнее хранилище. Как следствие возникает бремя эксплуатации базы данных, которая при этом не должна быть единой точкой отказа или бутылочным горлышком в системе.
Однако, есть альтернатива этому подходу. Возможно безопасно и надёжно хранить данные сессии в браузерной куке у самого пользователя, если заверить данные сессии криптографической подписью. Если вдобавок к этому данные ещё и зашифровать, то тогда содержимое сессии не будет доступно пользователю. Главное достоинство этого способа хранения в том, что он не требует централизованной базы данных для сессий со всеми вытекающими из этого плюсами в виде надёжности, скорости и масштабирования.
Описание механизма
Эта идея не нова и реализована во множестве фрэймворков и библиотек для различных языков программирования. Вот пара примеров:
Стоит заметить, в 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, ...) исходное сообщение не пропускается через блочный шифр, вместо этого блочный шифр используется для образования гамма-последовательности, и тогда длина шифротекста точно соответствует длине исходного сообщения, что лучше подходит для описываемой задачи.
Примеры использования
Небольшой скрипт, иллюстрирующий работу с этим хэндлером:
<?php
require_once("vendor/autoload.php");
header('Content-Type: text/plain');
$secret = '********************';
$handler = new VladislavYarmakStoragelessSessionCryptoCookieSessionHandler($secret);
session_set_save_handler($handler, true);
session_start();
if ($_GET) {
foreach ($_GET as $key => $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: VladislavYarmakStoragelessSessionCryptoCookieSessionHandler
public: true
arguments: ['reallylongsecretplease']
В качестве реального демо я подключил этот хэндлер сессий к первому пришедшему на ум веб-приложению, которое использует сессии. Им оказалось DokuWiki: wiki.vm-0.com. На сайте работает регистрация и логин, а работу сессий можно наблюдать в куках.
Благодарю за внимание и надеюсь, что эта статья поможет развитию ваших проектов.
Автор: YourChief