Поиск по репозиторию нашёл Function.constructor. Кажется, подозрение подтвердилось. Но что именно мы доказали? Динамическое создание функции разрешено языком JavaScript. Чтобы объяснить опасность, нужно показать, откуда пришёл её текст, будет ли она вызвана и какие возможности получит.
Это обычная задача на чтение потока данных. В CTF такой подход помогает найти уязвимость; при проверке внешнего проекта — восстановить устройство загрузчика и условия его работы.
Что превращается в код
Рассмотрим сокращённый учебный фрагмент. Он дан для чтения, не для запуска: функция объявлена, но не вызывается; реальный HTTP-клиент и адрес не заданы. Предположим, что при вызове клиент вернул бы текст ответа в response.data:
| |
Название loadSettings обещает загрузку настроек. Реальная последовательность другая. Ответ HTTP-запроса попадает в body. Эта строка становится телом новой функции. Затем функция вызывается с аргументом require.
Конструктор Function компилирует переданную строку как тело функции; само создание функции ещё не исполняет её тело. В примере выполнение происходит на последней строке. Новая функция не захватывает локальную область видимости создавшей её функции так, как обычное замыкание. Поэтому явная передача require — существенная деталь. [1]
В типичном CommonJS-окружении Node.js это даёт переданному коду возможность обращаться к доступным модулям. Но пределы действий определяются реальной средой и правами процесса, а не словом RCE в отчёте. [2]
В реальном разборе нужно проверить и тип response.data: HTTP-клиент может вернуть уже разобранный объект, а не исходную строку. Function приводит аргумент тела к строке, после чего проверяет синтаксис. Ошибка на этом этапе не даст дойти до apply. Это ещё одна причина не объявлять успешное исполнение по одному имени свойства. [1]
Как оформить доказательство
Для такой находки полезна короткая цепочка:
| |
Последняя строка не обесценивает находку. Она отделяет установленный механизм от событий, для которых пока нет свидетельств.
Если функцию вызывает код на верхнем уровне модуля, нужно проследить путь импорта от точки входа. Наличие подозрительного файла в src/ ещё не означает, что он используется текущей сборкой. Нужно также проверить ранние ошибки, условия, обработчики исключений и доступность сетевого ответа.
eval, Function и Function.constructor
eval и Function различаются по области видимости и правилам выполнения, однако обе конструкции могут превратить строку в код. Поэтому при аудите важнее происхождение строки, чем выбор одного из этих механизмов. [1][3]
Выражение Function.constructor в обычной, не изменённой среде JavaScript обращается к тому же конструктору Function. Но автоматическое правило, ищущее только new Function(, может пропустить эту запись. Поиск текста полезен как первый фильтр; он не равен анализу семантики. [1][4]
Отдельная ошибка — смешивать браузер и Node.js. Браузерные строковые таймеры и политика CSP относятся к веб-среде. В Node.js setTimeout ожидает функцию и при передаче другого типа выдаёт ошибку. Браузерная CSP не защищает запущенный на ноутбуке Node-процесс. [5][6]
Деталь API может изменить вывод
В одном из прежних фрагментов разбора встречалась конструкция следующего вида:
| |
В стандартном API Axios второй аргумент post — данные запроса, а конфигурация передаётся третьим. Следовательно, такой фрагмент сам по себе не устанавливает отправку HTTP-заголовка x-secret-header: он описывает объект в теле запроса. Заголовки могут добавляться другими настройками, но это нужно проверить отдельно. [7]
Для расследования различие практическое. Искомая строка может оказаться в другом месте сетевого обмена, а ожидаемый сервером заголовок вообще не отправиться. Это не делает вызов динамического кода безопасным, но влияет на то, могла ли сработать конкретная ветвь.
Что делать с находкой
Сначала проследите путь значения до исполнения. Зафиксируйте файл, строки, точку входа и условия вызова. Не обращайтесь к обнаруженному адресу ради любопытства: ответ сегодня не доказывает, какой ответ был получен во время инцидента.
В собственном приложении загрузку настроек обычно стоит проектировать как получение данных: разбор JSON, проверка схемы и явное применение допустимых полей. Замена eval на new Function не решает проблему доверия.
Если действительно требуется исполнение внешних программ, нужна отдельная архитектура ограничения полномочий. node:vm не следует считать защитной песочницей: документация Node.js прямо предупреждает, что этот модуль не предназначен для безопасного выполнения недоверенного кода. [8]
Задача: исполнение или только подготовка?
Файлы для самостоятельного разбора доступны в синтетическом практикуме.
Сравните три учебные строки независимо друг от друга. Предполагаются обычные встроенные функции JavaScript и CommonJS-окружение с доступным require; при разборе C учитывайте, что создание функции может завершиться ошибкой:
| |
Показать разбор
Разбор. В A строка разбирается как JSON; само это действие не исполняет её как JavaScript, хотя дальнейшее использование данных тоже требует проверки. В B создаётся функция с постоянным телом, но в показанной строке не вызывается. В C ответ передаётся конструктору как тело функции; если создание функции успешно, она сразу вызывается. Для C нужно выяснить происхождение response, достижимость строки и среду выполнения.
Ни одному из этих выводов не требуется слово «почти всегда». Достаточно назвать конкретное действие и его условия.
Источники
[1] ECMAScript: конструктор Function и CreateDynamicFunction.
[2] Node.js: CommonJS modules.
[3] ECMAScript: eval.
[4] ECMAScript: свойства Function.prototype.
[5] Node.js: Timers.
[6] W3C: Content Security Policy.
[7] Axios: сигнатуры методов в официальном репозитории, документация API.