13 авг. 2025 г.·5 мин. чтения

Скан риска RCE для Node‑приложений: быстро находите опасный код

Скан на риск RCE для Node‑приложений: обнаруживает `eval`, небезопасные вызовы child_process, шаблонные инъекции и рискованные динамические импорты, часто встречающиеся в AI‑сгенерированном коде.

Скан риска RCE для Node‑приложений: быстро находите опасный код

Как RCE выглядит в реальном Node‑приложении

Remote code execution (RCE) означает, что злоумышленник может заставить ваш сервер выполнить код, которого вы не намеревались запускать. Не просто прочитать файл или украсть токен, а выполнить команды или загрузить код так, чтобы получить контроль. Если приложение доступно из интернета, RCE — один из самых быстрых путей от мелкой ошибки к полному захвату.

В Node‑приложениях RCE часто проявляется, когда недоверенный ввод обрабатывают как инструкции. Классический пример — функция «запусти инструмент за меня»: пользователь отправляет текст, сервер собирает shell‑команду, и child_process её выполняет. Если код не жёстко контролирует допустимое, специально подготовленный запрос может превратить команду во что‑то иное.

AI‑сгенерированный код чаще содержит рискованные сокращения, потому что стремится быть полезным. Он может добавить eval для «парсинга» данных, собирать shell‑команды через шаблонные строки или динамически подгружать модули по параметру запроса. Такие паттерны годятся для демо, но опасны в продакшене.

Риск‑скан — быстрый способ найти код, который выглядит так, будто может выполнить ввод. Он показывает, куда смотреть в первую очередь, но не даёт 100% гарантии безопасности. Каждое находка всё равно требует проверки человеком, чтобы подтвердить, может ли пользовательский ввод реально добраться до опасной строки.

Злоумышленники обычно достигают RCE через повседневные точки входа:

  • HTTP‑запросы (query, body, headers)
  • Загрузки файлов (имена, пути, содержимое)
  • Вебхуки (сторонние «события», которым вы слишком доверяете)
  • Админ‑панели (меньше контроля, мощные действия)
  • Фоновые задания (сообщения из очередей или ввод из cron)

Где в код попадает недоверенный ввод

Большинство багов RCE начинаются одинаково: приложение обращается с чужими данными так, будто они безопасны. При сканировании Node‑приложения на риск RCE начните с картирования всех мест, куда может попасть данные, даже если они «обычно» приходят от вашей команды.

Очевидные точки входа — HTTP‑запросы. Всё, что пользователь может изменить, считается недоверенным: query‑строки, параметры маршрута, JSON‑тела, формы, заголовки (особенно те, что используются для feature‑флагов или режимов отладки), cookie, сессии и загружаемые файлы (включая имена файлов).

Ввод приходит не только по сети. Быстрые прототипы часто содержат вспомогательные скрипты и админ‑фичи, обходящие обычные проверки, которые затем подсоединяют в продакшен. Обратите внимание на фоновые задания и cron‑задачи, читающие данные из БД, сообщения из очередей, полезные нагрузки вебхуков, админ‑панели и «только для внутреннего пользования» эндпоинты, а также CLI‑скрипты, принимающие аргументы или читающие переменные окружения.

«Внутренние инструменты» всё ещё важны. Токены воруют, VPN используют неправильно, и одна утекшая админ‑cookie может превратить внутренний эндпоинт в доступный из интернета.

Полезная мысленная модель:

input -> parsing -> execution

Парсинг — это место, где меняются типы (строка в JSON, JSON в объект, объект в шаблон). Выполнение — где риск резко возрастает (сбор команд, оценка кода, загрузка модулей). Поддерживающий endpoint, который принимает JSON вроде { "report": "weekly" }, может стать источником RCE позже, если кто‑то добавит child_process или шаг рендеринга шаблона, который использует то же поле.

Простой план для сканирования рисков RCE

Не нужны сложные инструменты, чтобы скан был полезен. Начните с поиска форм кода, которые чаще всего приводят к «выполнению того, что прислал пользователь», особенно в AI‑сгенерированном коде, где проскальзывают небезопасные сокращения.

Сначала пройдитесь по ключевым словам, чтобы собрать короткий список файлов для ревью:

rg -n "\b(eval)\b|new Function|child_process|exec\(|execSync\(|spawn\(|spawnSync\(|fork\(|vm\.run|ejs|pug|handlebars|nunjucks|import\(|require\(" .

Затем превратите попадания в карту потоков ввода. Для каждой находки ответьте на два вопроса:

  • Какие данные могут дойти до этой строки?
  • Кто контролирует эти данные?

Отслеживайте распространённые источники: параметры запросов, заголовки, cookie, payload вебхуков и всё, что читается из БД и изначально пришло от пользователей.

Для приоритизации фокусируйтесь на комбинации достижимости и силы: маршруты, доступные из интернета (включая вебхуки), любое использование выполнения команд ОС или загрузки кода (child_process, vm, динамический import/require), и места, где строки собирают из ввода без allowlist. Подтверждайте, что код действительно живёт в продакшене: мёртвый код и скрипты только для разработки ниже по приоритету, чем участки, которые реально обрабатывают запросы.

Найдите eval и Function, которые могут выполнить ввод

Начните с поиска мест, где код строят из строк. Основные подозреваемые — eval() и new Function(). Они превращают текст в то, что сервер выполняет. Если злоумышленник может повлиять на этот текст, он может попытаться запустить собственный код.

Отмечайте паттерны вроде:

  • eval(userInput) или eval(someVar)
  • new Function("return " + expr)()
  • setTimeout("doThing(" + x + ")", 0) и setInterval("...", 1000)
  • «оценочные движки», которые конкатенируют строки перед выполнением

«Он оценивает только небольшие выражения» всё равно опасно. На первый взгляд безобидный калькулятор может получить доступ к process.env, читать файлы, совершать сетевые вызовы или хуже, если строка может быть сформирована атакующим.

Ввод не обязательно приходит прямо из тела запроса. Он часто проскальзывает через конфиги, поля в базе, контент CMS, feature‑флаги или шаблоны, где хранятся редактируемые пользователями правила. В быстрых прототипах иногда встречается простая «машина правил» вроде eval(dbRow.rule), которая работала в демо и попала в релиз.

Безопасные замены зависят от цели. В общем случае предпочитайте фиксированный набор операций, сопоставленный с реальными функциями, или храните правила как данные (например, JSON) и интерпретируйте их через строгую валидацию. Если действительно нужен язык выражений — используйте настоящую библиотеку‑парсер и выполняйте только поддерживаемые узлы.

Проверьте child_process на инъекции команд

Если приложение использует child_process для запуска системных команд, считайте это первоочередным риском RCE. Опасность не в самом использовании shell, а в том, что любой контролируемый пользователем текст становится частью команды.

Наибольший риск дают exec и execSync, а также spawn с shell: true. Их легко неправильно использовать, потому что они принимают одну строку команды. Если вы собираете строку через + или шаблонные строки, пользователь может вставить дополнительные операторы (;, &&, |) и выполнить что‑то другое.

Типичная ошибка в реальном мире — endpoint «конвертировать файл», который делает exec( convert ${req.body.path} -resize 200x200 out.png ). Если path содержит image.png; cat /etc/passwd, shell увидит две команды.

Более безопасный паттерн — не использовать shell и передавать массив аргументов. Например: spawn('convert', [inputPath, '-resize', '200x200', outPath], { shell: false }). Всё равно валидируйте inputPath, но вы убираете большую часть трюков парсинга shell.

При сканировании ищите тревожные признаки: строки команд, собранные из полей запроса, shell: true, ручная склейка аргументов (args.join(' ')) и «эскейп‑хелперы», которые заменяют только пару символов.

Если добавляете логирование для триажа, логируйте важное (осторожно): имя команды, массив args (или полную строку, если необходимо) и откуда пришёл ввод (маршрут и имя поля). Не сливайте секреты в логи.

Обнаружьте пути серверной шаблонной инъекции

Быстрая обработка для основателей
Большинство проектов FixMyMess выполняются за 48–72 часа после аудита.

Server‑side template injection (SSTI) случается, когда приложение создаёт шаблон из пользовательского ввода и рендерит его на сервере. Если движок шаблонов умеет выполнять выражения, атакующий может превратить «пользовательское сообщение» в выполнение кода.

Простой пример: вы позволяете пользователям сохранять email‑шаблон и компилируете/рендерите то, что они ввели. Если движок поддерживает {{ someExpression }} или вызовы хелперов, пользователь может прочитать секреты, вызвать функции или пробраться к другим опасным API.

При быстром сканировании ищите места, где строка, контролируемая пользователем, становится шаблоном, partial, layout или именем хелпера. Частые паттерны: компиляция или рендеринг пользовательского ввода напрямую, передача req.body как locals (res.render("view", req.body)), выбор partial/layout по имени через конкатенацию путей, динамическая регистрация хелперов из данных запроса или рендеринг сохранённого «markdown»/«handlebars»/«ejs» из поля формы без слоя безопасности.

Обычно рабочие защиты:

  • Не компилируйте шаблоны из сырого пользовательского текста. Храните контент, но рендерьте его как простой текст или безопасное подмножество разметки.
  • Держите строгую карту переменных для шаблонов. Не передавайте целые объекты вроде req, res или process.
  • Относитесь к именам шаблонов как к файловым путям: allowlist известных шаблонов и отвергайте всё остальное.

Проверьте рискованные динамические импорты и загрузку модулей

Динамическая загрузка модулей удобна, но это частый путь, по которому RCE попадает в Node‑приложения, особенно в быстро сгенерированном коде. Если часть имени модуля или пути приходит от пользователя, считайте это недоверенным вводом.

Самые опасные паттерны выглядят безобидно: import(userInput), require(userInput) или require('./plugins/' + name). Часто предполагают, что «загружаются только локальные файлы», но атакующий может воспользоваться трюками с путями, неожиданными местами файлов или получить доступ к коду, который вы не хотели экспонировать.

Сканируйте на:

  • import(something), где something не строковый литерал
  • require(something), где something собирается из переменных
  • require(path.join(base, userValue)) и любые риски обхода вроде ../
  • фичи «плагинов», которые загружают модули по имени
  • чтение имени файла из запроса, БД или конфига и затем загрузка его

Если динамическая загрузка действительно нужна, делайте её явной. Используйте жёсткий allowlist, который мапит имена на точные пути, отвергайте всё, что не в мапе, и ни в коем случае не передавайте сырой ввод в require() или import().

Не пропустите сопутствующие проблемы, облегчающие RCE

Снизить будущие сюрпризы RCE
Добавим guardrails и тесты, чтобы `eval`, `exec` и небезопасные импорты не вернулись.

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

Начните с секретов. Быстрые прототипы часто оставляют API‑ключи, JWT‑секреты, URL баз данных и облачные токены в открытом виде, в примерах env‑файлов или логах. Если атакующий получит даже минимальное выполнение кода, открытые секреты позволят ему быстро прыгнуть к вашим данным и другим системам.

Права важны не меньше. Если приложение запускается под пользователем, который может слишком многое читать и записывать, RCE превращается в «изменить сервер». Следите за широкими правами на файлы, доступными директориями приложения и чрезмерно мощными ролями в облаке.

Проверьте также настройки продакшена, которые тихо открывают двери: режим разработки в проде, включённые флаги отладки (подробные ошибки, стек‑трейсы, hot reload), оставшиеся скрытые админ‑маршруты, небезопасный CORS и директории для загрузок/временных файлов, которые доступны для записи и экспонированы.

Наконец, зависимости могут быть самым слабым звеном. Устаревшие пакеты с известными CVE по RCE способны превратить безобидный маршрут в компрометацию. Быстрый проход по lockfile и предупреждениям — часть работы.

Распространённые ошибки при попытках исправить RCE

Самый быстрый путь потратить время при исправлении RCE — лечить видимый симптом, не доказав, как код достигается. Оптимальный на вид helper всё ещё опасен, если он стоит за маршрутом, middleware, cron‑работой или вебхуком, принимающим внешний ввод.

Одна из ловушек — предположение «он не достижим», потому что в UI нет кнопки. Проследите путь: request -> router -> middleware -> controller -> helper. То же самое для фоновых воркеров и обработчиков вебхуков. Многие реальные инциденты начинаются в кодовых путях, которые редко тестируют: платежный вебхук, OAuth‑колбэк или внутренняя очередь задач.

Ещё одна ошибка — лечить симптом вместо причины. Экранирование вывода или добавление кодирования не делает eval, Function, exec или spawn безопасными, если туда может попасть недоверенный ввод. Если скан находит динамическое выполнение, цель обычно — удалить его, а не «сделать санитайз».

Regex‑подходы «санитизации» — тупиковый путь. Если фикс зависит от «заблокируй эти символы», считайте, что кто‑то обойдёт это пробелами, кавычками, метасимволами shell или кодировками.

Привычки, которые предотвращают появление тех же ошибок снова:

  • Докажите достижимость, прослеживая маршруты, middleware, воркеры и вебхуки
  • Заменяйте динамическое выполнение фиксированными командами, шаблонами или проверенными библиотеками
  • Валидируйте ввод по типу и намерению (ID, enum, строгие схемы), а не регулярками угадывания
  • Добавляйте тесты с вредоносными payload'ами, чтобы проблема не вернулась

Быстрый чек‑лист перед релизом

Используйте это после скана и снова перед выпуском. Цель проста: ничего на сервере не должно превращать пользовательский ввод в код, shell‑команду или путь модуля.

  • Нет eval, new Function или строковых setTimeout/setInterval в серверном коде.
  • Нет exec или execSync. Для spawn/execFile передавайте args как массив и не включайте shell: true.
  • Не компилируйте серверные шаблоны из пользовательского текста.
  • Любая динамическая загрузка модулей — по allowlist и без приёма путей, построенных пользователем.
  • Валидация ввода происходит на границах (HTTP‑хендлеры, потребители очередей, вебхуки) с чёткими правилами по полю.

Простой тест здравомыслия: представьте, что атакующий контролирует один query‑параметр, один заголовок и одно JSON‑поле. Может ли какое‑то из этих значений дойти до исполнителя кода (eval), раннера команд (child_process), компилятора шаблонов или пути импорта без отклонения?

Пример: небольшая фича, которая случайно становится RCE

Подготовка к деплою
Мы чистим конфиги сборки, права и окружение, чтобы вы могли безопасно деплоить.

Типичная реальная неразбериха — внутренняя админ‑страница, сгенерированная AI, с полем «Run maintenance». Идея безобидна: введите reindex-users или clear-cache, нажмите Run, и сервер выполняет это.

Проблема в подключении фичи. Endpoint часто берёт req.body.command и передаёт в child_process.exec(), или собирает путь к скрипту и делает import() этого файла. Если эндпоинт доступен (слабая аутентификация, утёкшая админ‑cookie, пропущенная проверка роли), атакующий попробует reindex-users && cat .env или ../../../../tmp/payload — и вы получите RCE.

Скан обычно быстро помечает: child_process.exec() с данными из запроса (даже после «санитизации»), динамические import()/require() из строк, рендеринг шаблонов, компиляция пользовательского ввода и оставшиеся хелперы вроде eval() или new Function().

После скана подтвердите вручную: доступен ли маршрут без строгой admin‑проверки, действительно ли ввод контролируется пользователем и что именно запускается на сервере (shell, node или другой раннер скриптов)? Опасный код часто спрятан за «временными» feature‑флагами.

Что исправлять в первую очередь, чтобы убрать примитивы выполнения:

  1. Удалите поле свободного текста и замените его allowlist‑ом именованных действий.
  2. Поменяйте exec на более безопасные API (или выполняйте работу в процессе обычными функциями).
  3. Закройте маршрут под реальной admin‑ролью и логируйте каждое действие.
  4. Запускайте сервис с минимальными правами и удалите секреты из окружения рантайма.

Результат прост: меньше путей, ведущих к коду, и никакого прямого способа выполнить входной текст.

Следующие шаги: превратите находки в безопасный продакшен‑код

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

Практичный способ решения — пометить каждую проблему:

  • Patch: текущий подход можно сделать безопасным жёсткой валидацией, allowlist и более безопасными API.
  • Refactor: фича нужна, но дизайн приглашает к ошибкам.
  • Remove/replace: фича существует главным образом потому, что генератор добавил её.

После правок напишите короткую security‑заметку в репозитории. Держите её простой и конкретной: нет eval/Function с данными запроса, никакие строки из пользователей в child_process, шаблоны не компилируются из пользовательского ввода, динамические импорты по allowlist. Это поможет будущему вам и ускорит ревью кода.

Добавьте лёгкие барьеры, чтобы те же проблемы не вернулись. Сохранённый поиск или чек‑лист ревью по eval, new Function, child_process.exec, компиляции шаблонов и динамическому import() из переменных ловит много ошибок.

Если вы унаследовали прототип от инструментов вроде Lovable, Bolt, v0, Cursor или Replit и хотите быстрый второй взгляд — FixMyMess (fixmymess.ai) фокусируется на диагностике и исправлении таких рискованных паттернов, а затем проверяет исправления экспертной ручной проверкой, чтобы приложение выдержало продакшен.

Часто задаваемые вопросы

Что именно считается RCE в Node‑приложении?

RCE (remote code execution) — это ситуация, когда кто‑то может заставить ваш сервер выполнить код или команды, которых вы не планировали. Это обычно хуже утечек данных, потому что может привести к полному контролю над приложением, сервером и всеми секретами, к которым есть доступ.

Как быстро выполнить скан на риск RCE без сложных инструментов?

Начните с того, где в приложение попадает ненадёжные данные: HTTP‑запросы, вебхуки, загрузки файлов, админ‑инструменты и фоновые задания. Затем ищите «примитивы выполнения», такие как eval, new Function, child_process, компиляция шаблонов и динамический import()/require(), и прослеживайте, может ли туда попасть пользовательский ввод.

Скажет ли мне скан ключевых слов, уязвим ли я точно?

Нет. grep‑подобный поиск выявляет подозрительные формы кода, но не подтверждённые эксплойты. Нужно проследить поток данных и подтвердить достижимость в продакшене: некоторые попадания — это мёртвый код или участки, работающие только с доверенными данными.

Почему внутренние админ‑эндпоинты часто становятся проблемой RCE?

Даже «внутренние» эндпоинты становятся доступными при краже токенов, утечке cookie или неверной конфигурации. Относитесь к внутренним инструментам как к реальной поверхности атаки: строгая аутентификация, жёсткая валидация ввода и отсутствие свободного выполнения команд.

Всегда ли `eval()` — это уязвимость, или он может быть безопасным?

Опасно всегда, когда строка, которую выполняют, может быть как‑то изменена пользователем — даже косвенно через БД, конфиг, CMS или feature‑флаги. Чаще всего безопасный путь — удалить строковую оценку и заменить её небольшим allowlist операций, реализованных в виде реальных функций.

В чём реальная опасность `child_process.exec()` и строк команд?

exec и execSync особенно опасны, потому что выполняют командную строку через shell, что даёт возможность инъекции дополнительных операторов. Лучше использовать невложенное выполнение с массивом аргументов (spawn, execFile) и всё равно валидировать имена файлов, идентификаторы и т. п.

Как понять, может ли использование шаблонного движка привести к RCE?

SSTI возникает, когда вы компилируете или рендерите шаблон из пользовательского текста, а движок шаблонов поддерживает выражения или вызовы хелперов. Безопасный подход — хранить пользовательский текст как контент и рендерить его как простой текст или ограниченное подмножество разметки, а не компилировать как серверный шаблон.

Почему динамические `import()` или `require()` считаются риском RCE?

Риск появляется, когда часть имени модуля или пути приходит из пользовательских данных, даже если кажется, что загружаются только локальные файлы. Практический фикс — маппинг допустимых имён на точные пути в коде и отказ от передачи сырого ввода в require()/import().

Можно ли просто «очистить» ввод, чтобы убрать риск RCE?

Не надёжно. Блокировка отдельных символов вроде ; или && легко обходится кодировкой, пробелами или разными правилами парсинга shell. Это не решает проблему шаблонной инъекции или динамического выполнения. Безопаснее убрать примитив выполнения или ограничить его фиксированными, allowlist‑действиями.

Что исправлять первым, если скан выявил несколько опасных мест?

В приоритете — всё, что доступно из интернета (включая вебхуки) и может выполнить код, запустить ОС‑команды, скомпилировать шаблоны или загрузить модули динамически. Если унаследовали AI‑сгенерированный код и нужно быстрое практическое ревью, команды вроде FixMyMess помогают диагностировать и исправлять такие паттерны и проверяют исправления вручную.