- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Реализации, которые я рассмотрел:

Репозиторий Комментарий
github.com/Coercive/Cookie [5] Фактически не библиотека для работы с сессиями вовсе. Ставит шифрованную куку, не подписывая её.
github.com/stevencorona/SessionHandlerCookie [6] Ближе всего к требованиям, но всё же имеет значительные недостатки:

  • Потенциально уязвима к атакам по времени [7] из-за прямого сравнения хэша [8] с образцом
  • Нет шифрования
  • Нет тестов
  • Неэкономная упаковка куки
  • Время истечения куки не хранится со значением и не охвачено подписью. Это значит. что клиент, единожды получив данные в сессии, может воспроизводить их бесконечно.
  • Мелкие баги: read() после write() в рамках одного выполнения скрипта показывает не то, что записано и пр.

github.com/mapkyca/Encrypted-Client-Side-Sessions [9]

Также я смотрел реализацию хранения сессий в куках в фрэймворке Slim версии 2.x, но там нет ни подписи, ни шифрования. О чём авторы сразу и предупреждают.

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

После всех поисков я решил реализовать такую библиотеку самостоятельно.

Собственная реализация

Packagist: packagist.org/packages/snawoot/php-storageless-sessions [12]
Github: github.com/Snawoot/php-storageless-sessions [13]
Установка из 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 [14].

Пример интеграции в Symfony:

framework:
    session:
        handler_id:  session.handler.cookie

services:
    session.handler.cookie:
        class:     VladislavYarmakStoragelessSessionCryptoCookieSessionHandler
        public:    true
        arguments:    ['reallylongsecretplease']

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

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

Автор: YourChief

Источник [16]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/php-2/251746

Ссылки в тексте:

[1] Python/Django: https://docs.djangoproject.com/en/1.10/topics/http/sessions/#using-cookie-based-sessions

[2] Ruby/Ruby on Rails: http://api.rubyonrails.org/classes/ActionDispatch/Session/CookieStore.html

[3] HMAC-подпись: https://ru.wikipedia.org/wiki/HMAC

[4] SessionHandlerInterface: http://php.net/manual/ru/class.sessionhandlerinterface.php

[5] github.com/Coercive/Cookie: https://github.com/Coercive/Cookie

[6] github.com/stevencorona/SessionHandlerCookie: https://github.com/stevencorona/SessionHandlerCookie

[7] атакам по времени: https://ru.wikipedia.org/wiki/%D0%90%D1%82%D0%B0%D0%BA%D0%B0_%D0%BF%D0%BE_%D0%B2%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%B8

[8] прямого сравнения хэша: https://github.com/stevencorona/SessionHandlerCookie/blob/master/src/SessionHandler/Storage/SecureCookie.php#L72

[9] github.com/mapkyca/Encrypted-Client-Side-Sessions: https://github.com/mapkyca/Encrypted-Client-Side-Sessions

[10] Отсутствие подписи: https://github.com/mapkyca/Encrypted-Client-Side-Sessions/blob/master/cookie.class.php#L57-L78

[11] Использование для шифрования статического IV: https://github.com/mapkyca/Encrypted-Client-Side-Sessions/blob/master/cookie.class.php#L18

[12] packagist.org/packages/snawoot/php-storageless-sessions: https://packagist.org/packages/snawoot/php-storageless-sessions

[13] github.com/Snawoot/php-storageless-sessions: https://github.com/Snawoot/php-storageless-sessions

[14] https://vm-0.com/sess.php: https://vm-0.com/sess.php

[15] wiki.vm-0.com: https://wiki.vm-0.com/

[16] Источник: https://habrahabr.ru/post/325452/