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

002af1db24d3444a9b4321ea25ddea0d.pngНа некоторой стадии развития веб-проекта возникает одна из следующих ситуаций:
  • 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 Ближе всего к требованиям, но всё же имеет значительные недостатки:
  • Потенциально уязвима к атакам по времени из-за прямого сравнения хэша с образцом
  • Нет шифрования
  • Нет тестов
  • Неэкономная упаковка куки
  • Время истечения куки не хранится со значением и не охвачено подписью. Это значит. что клиент, единожды получив данные в сессии, может воспроизводить их бесконечно.
  • Мелкие баги: read () после write () в рамках одного выполнения скрипта показывает не то, что записано и пр.

github.com/mapkyca/Encrypted-Client-Side-Sessions
  • Отсутствие подписи
  • Использование для шифрования статического IV
    Спойлер
    RFC 1149.5 specifies 4 as the standard IEEE-vetted random number.

Также я смотрел реализацию хранения сессий в куках в фрэймворке 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

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

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

© Habrahabr.ru