четверг, 12 июня 2014 г.

Как вы возможно помните, я пишу хранилище для временных рядов - akumuli. В данный момент я занят самим "движком" для хранения данных, однако планирую и frontend, который будет уметь записывать и считывать данные по сети. Для этого, мне нужен какой-нибудь механизм сериализации, само собой - он должен быть быстрым и эффективным, но самое главное - он должен быть безопасным. В идеале - сервер должен уметь торчать в интернет, с оговорками, вроде ограничения количества сообщений от одного клиента в time-frame.
И тут все очень плохо. Очень многие библиотеки сериализации проектировались без учета требований безопасности и позволяют положить сервис одним сообщением. За примерами далеко ходить не надо, вот, например, один товарищ с RSDN написал библиотеку сериализации - YAS (Yet Another Serializer) для С++. Это header only библиотека, умеющая сериализовать как стандартные контейнеры, так и контейнеры из boost и Qt. Можно также сериализовать пользовательские типы, интерфейс похож на boost::serialization. Библиотека YAS заявлена как drop-in replacement (correct me if I wrong) для boost::serialization, что хорошо, и работает заметно быстрее оного, что тоже хорошо. Что не хорошо, так это возможность уронить сервис одним сообщением:


Внимательно смотрим на 13-ю строку. Несложно догадаться, что нам достаточно передать вместо длины списка очень большое число, чтобы сервис упал или ушел в своп, при этом элементы списка можно не передавать вовсе! Вызов list.resize попытается создать нужное количество элементов, сделав столько аллокаций, сколько мы ему скажем, причем в выделенные участки памяти он будет писать, а значит память реально будет выделяться системой. При этом YAS не позволяет задать максимальный размер сообщения и ограничить максимальную длину списка. Этот фокус можно повторить для других типов, поддерживаемых этой библиотекой - deque, stable_vector из boost, может еще что-то.

Можно подумать, что это проблема только одной библиотеки, но на самом деле, такая фича у библиотек сериализации встречается часто (при том, что это самая очевидная ошибка из всех, что они могут сделать). Вот, например, cereal - второй по счету результат в выдаче на github по запросу serialization для языка С++.

Очень похоже, не правда ли? Я проверил, там нигде размер не проверяется.
А теперь внимание, простой вопрос - если настолько простая дыра в безопасности лежит на виду в проекте, у которого больше 300 лайков на github и за которым следят больше сорока человек, то сколько таких дыр будет в проекте попроще? :)
К слову, C++ реализация protocol buffers ничем подобным не страдает, там можно ограничить максимальный размер сообщения сверху (64Мб по умолчанию, но можно задать свое значение), а максимальная длина их base-128 variant-ов ограничена 64-мя битами.

Бывают примеры и посложнее, вот, например реализация message pack для Go, там, на 179-й строке есть функция unpack, которая вызывает сама себя рекурсивно, в зависимости от того, что встретит в потоке данных. Это нужно для того, чтобы парсить всякие вложенные структуры данных, вроде массива строк, т.е. глубина рекурсии зависит от входящих данных! Можно очень легко создать такое сообщение, которое заставит эту функцию вызвать себя очень много раз подряд и сожрать очень много памяти под стек, вообще сколько угодно памяти (в Go не получится переполнение стека из-за особенностей рантайма, но стек расти все равно будет). Если бы это было написано не на Go, а скажем, на Java, мы бы могли получить переполнение стека куда быстрее, чем сожрать всю память :)

В общем, нужно писать хороший и безопасный код и не писать плохой, а также стараться использовать поверенные решения и анализировать сторонний код, используемый в вашем приложении. Спасибо за внимание! :)

Комментариев нет: