Pull to refresh

PHP: Хранение сессий в защищённых куках

Reading time 5 min
Views 26K
На некоторой стадии развития веб-проекта возникает одна из следующих ситуаций:

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

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

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

Описание механизма


Эта идея не нова и реализована во множестве фрэймворков и библиотек для различных языков программирования. Вот пара примеров:


Стоит заметить, в Ruby on Rails делают большую ставку на производительность этого механизма в сравнении со всеми остальными методами хранения сессий и используют его по умолчанию.

Большинство имеющихся реализаций работают следующим образом: записывают в какую-то куку строку, содержащую время истечения сессии, данные сессии и HMAC-подпись времени истечения и данных. При запросе клиента кука читается соответствующим обработчиком, затем проверяется подпись и сравнивается текущее время с временем истечения сессии. Если всё совпадает, обработчик возвращает данные сессии в приложение.

Однако, шифрование куки в распространённых реализациях этого механизма отсутствует.

Сравнение с классическим подходом


В итоге, хранение сессий в куках имеет следующие достоинства:

  • Возрастает производительность веб-приложения, так как небольшая криптографическая операция дешевле сеанса сетевого обмена или доступа к диску для извлечения данных сессии.
  • Возрастает надёжность веб-приложения, так как оно не зависит от внешнего KV-хранилища. Даже если хранилище сессий обеспечено средствами отказоустойчивости, это не наделяет его абсолютной стабильностью: переключение требует времени, а часть проблем (такие как ухудшение сетевой связности между регионами) и вовсе неискоренимы. Зачастую же сессии и вовсе хранятся на единственном сервере, являющимся единой точкой отказа всего веб-приложения.
  • Экономия ресурсов. Не нужно больше хранить сессии, а значит от этого выиграют и владельцы маленьких сайтов, у которых сократится дисковая активность, и освободят несколько серверов владельцы крупных веб-проектов.

Имеются и недостатки, куда же без них:

  • Возрастает объём данных, передаваемый клиентом
  • Имеется ограничение на размер данных в сессии, связанное с ограничениями на размер кук. Обычно это чуть меньше 4 КБ кодированных данных.
  • Клиент может откатить состояние сессии на любое выданное и подписанное ранее значение, криптоподпись которого ещё действительна в текущий момент времени.

Реализации для PHP


Когда я попытался отыскать что-то похожее для PHP, я с удивлением обнаружил, что не существует ни одной библиотеки, которая дотягивает до минимума требований:

  • Безопасность: отсутствие ошибок при использовании криптографии
  • Актуальная кодовая база: поддержка современных версий PHP, отсутствие deprecated-расширений в зависимостях (таких как mcrypt)
  • Наличие тестов: сессии — это один из фундаментальных механизмов, и в основе реального приложения нельзя использовать незрелый код

Кроме этого считаю вовсе не лишним:

  • Возможность шифрования: открытое хранилище сессии на клиенте, читаемое клиентом, не всем подходит.
  • Максимально компактное представление данных — ради минимизации оверхеда и запаса ёмкости сессии
  • Встраиваемость через SessionHandlerInterface

Реализации, которые я рассмотрел:
Репозиторий Комментарий
github.com/Coercive/Cookie Фактически не библиотека для работы с сессиями вовсе. Ставит шифрованную куку, не подписывая её.
github.com/stevencorona/SessionHandlerCookie Ближе всего к требованиям, но всё же имеет значительные недостатки:
  • Потенциально уязвима к атакам по времени из-за прямого сравнения хэша с образцом
  • Нет шифрования
  • Нет тестов
  • Неэкономная упаковка куки
  • Время истечения куки не хранится со значением и не охвачено подписью. Это значит. что клиент, единожды получив данные в сессии, может воспроизводить их бесконечно.
  • Мелкие баги: read() после write() в рамках одного выполнения скрипта показывает не то, что записано и пр.

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 VladislavYarmak\StoragelessSession\CryptoCookieSessionHandler($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:     VladislavYarmak\StoragelessSession\CryptoCookieSessionHandler
        public:    true
        arguments:    ['reallylongsecretplease']


В качестве реального демо я подключил этот хэндлер сессий к первому пришедшему на ум веб-приложению, которое использует сессии. Им оказалось DokuWiki: wiki.vm-0.com. На сайте работает регистрация и логин, а работу сессий можно наблюдать в куках.

Благодарю за внимание и надеюсь, что эта статья поможет развитию ваших проектов.
Tags:
Hubs:
+31
Comments 107
Comments Comments 107

Articles