О той уязвимости хеш-флуда в Node.js…
В начале июля этого года Node.js выпустил обновление безопасности для всех поддерживаемых веток, чтобы устранить уязвимость хеш-флуда. Этот промежуточный фикс имеет стоимость значительного снижения производительности запуска. Между тем V8 реализовал решение, которое избегает штрафа производительности.
В этом посте мы хотим предоставить некоторый контекст и историю уязвимости и окончательного решения.
Атака хеш-флуда
Хеш-таблицы — одна из самых важных структур данных в информатике. Они широко используются в V8, например, для хранения свойств объекта. В среднем добавление новой записи выполняется очень эффективно на 𝒪(1). Однако, хеш-конфликты могут привести к худшему случаю 𝒪(n). Это означает, что добавление n записей может занять до 𝒪(n²).
В Node.js, HTTP-заголовки представлены в виде JavaScript объектов. Пары имени и значений заголовков хранятся как свойства объекта. С помощью умело подготовленных HTTP-запросов злоумышленник мог бы произвести атаку отказа в обслуживании. Процесс Node.js становился бы неотзывчивым, будучи занят в худшем случае вставки в хеш-таблицу.
Эта атака была раскрыта еще в декабре 2011 года и показана как затрагивающая множество языков программирования. Почему же потребовалось так много времени, чтобы V8 и Node.js наконец-то решили эту проблему?
На самом деле, вскоре после раскрытия инженеры V8 начали работать с сообществом Node.js над смягчением. Начиная с версии Node.js v0.11.8, эта проблема была решена. Исправление ввело так называемое значение семени хеша. Значение семени хеша выбирается случайным образом при запуске и используется для инициализации каждого значения хеша в конкретном экземпляре V8. Без знания семени хеша злоумышленнику трудно достичь худшего случая, не говоря уже о разработке атаки, направленной на все экземпляры Node.js.
Вот часть коммита с исправлением:
Эта версия решает проблему только для тех, кто компилирует V8 самостоятельно или кто не использует snapshots. Предсобранная версия V8 на основе snapshots все еще будет иметь предсказуемые коды хеша строк.
Эта версия решает проблему только для тех, кто компилирует V8 самостоятельно или кто не использует snapshots. Предсобранная версия V8 на основе snapshots все еще будет иметь предсказуемые коды хеша строк.
Snapshot запуска
Snapshots запуска — это механизм в V8, который значительно ускоряет как запуск движка, так и создание новых контекстов (например, с помощью модуля vm в Node.js). Вместо того чтобы настраивать начальные объекты и внутренние структуры данных с нуля, V8 выполняет десериализацию из существующего snapshot. Обновленная сборка V8 с snapshot запускается менее чем за 3 мс и требует доли миллисекунды для создания нового контекста. Без snapshot запуск занимает более 200 мс, а создание нового контекста более 10 мс. Это разница на два порядка.
Мы рассмотрели, как любой внедряющий V8 может воспользоваться snapshot запуска в предыдущем посте.
Предсобранный snapshot содержит хеш-таблицы и другие структуры данных на основе значений хеша. После инициализации из snapshot семя хеша больше не может быть изменено без повреждения этих структур данных. Релиз Node.js, который включает snapshot, имеет фиксированное значение семени хеша, что делает смягчение неэффективным.
Именно об этом было явное предупреждение в сообщении коммита.
Почти исправлено, но не полностью
Вперед к 2015 году, проблема в Node.js сообщила о том, что создание нового контекста стало менее производительным. Неудивительно, что это произошло из-за отключения snapshot запуска как части смягчения. Но к тому времени не все участники обсуждения знали причину.
Как объясняется в этом посте, V8 использует генератор псевдослучайных чисел для создания результатов Math.random. Каждый контекст V8 имеет собственное состояние генератора случайных чисел. Это предотвращает предсказуемость результатов Math.random между контекстами.
Состояние генератора случайных чисел берётся из внешнего источника сразу после создания контекста. Не имеет значения, был ли контекст создан с нуля или десериализован из снимка.
Как-то состояние генератора случайных чисел было перепутано с хеш-началом. Как результат, предуготовленный снимок стал частью официального релиза с io.js v2.0.2.
Вторая попытка
Только в мае 2017 года во время внутренних обсуждений между V8, Google’s Project Zero и Google’s Cloud Platform мы поняли, что Node.js всё ещё уязвим для атак хеш-флудинга.
Первая реакция поступила от наших коллег Али и Майлса из команды, ответственной за Node.js на Google Cloud Platform. Они совместно с сообществом Node.js отключили снимок при старте по умолчанию снова. На этот раз они также добавили тестовый кейс.
Но мы не хотели оставлять это так. Отключение снимка при старте оказывает значительное влияние на производительность. С годами мы добавили множество новых языковых функций и сложных оптимизаций в V8. Некоторые из этих дополнений сделали запуск с нуля ещё более дорогим. Сразу после выпуска обновления безопасности мы начали работать над долгосрочным решением. Цель состояла в том, чтобы повторно включить снимок при старте, не став уязвимыми для нападений хеш-флудинга.
Из предложенных решений мы выбрали и реализовали наиболее прагматичное. После десериализации из снимка мы будем выбирать новое хеш-начало. Затронутые структуры данных затем будут перехешированы для обеспечения согласованности.
Как оказалось, в обычном снимке при старте на самом деле затрагивается мало структур данных. И к нашей радости, перехеширование хеш-таблиц было упрощено в V8 за это время. Добавляемая этим нагрузка незначительна.
Патч для повторного включения снимка при старте был объединён в Node.js. Он является частью последнего релиза Node.js v8.3.0 релиз.