Node.js Internals

VM Module: Песочница и изоляция

Представь: пользователи пишут плагины для твоего приложения. Как выполнить их код, не дав сломать весь сервер? VM Module - это первая линия защиты между trusted и untrusted кодом.

  • **Webpack/Rollup** выполняют конфигурационные файлы в изолированных контекстах (webpack.config.js может содержать произвольный JS)
  • **Jest/Vitest** используют VM для изоляции тестов - каждый тест получает свой глобальный объект, чтобы моки не протекали между файлами
  • **Figma Plugins** работают в isolated-vm - миллионы пользовательских скриптов выполняются безопасно на серверах Figma
  • **Cloudflare Workers** компилируют пользовательский код в V8 Isolates с лимитами CPU/RAM (до 50ms на запрос)

Зачем нужна изоляция кода

Модуль **vm** позволяет выполнять JavaScript-код в изолированных контекстах - отдельных V8-окружениях с собственными глобальными объектами. Это не полная изоляция (один процесс, одна память), но защита от случайного или намеренного доступа к глобальному скоупу хост-процесса.

**Ключевая идея:** VM создает новый глобальный объект, но работает в том же V8 Isolate - прототипы, конструкторы, setTimeout общие. Это **не песочница уровня ОС**, а логическая граница.

Use Cases

  • **Плагины и расширения** - пользователи пишут код для приложения (Webpack, Rollup, ESLint)
  • **Шаблонные движки** - выполнение пользовательских template expressions (Handlebars, Nunjucks)
  • **REPL и интерактивные среды** - Node.js REPL, онлайн-IDE, Jupyter-подобные ноутбуки
  • **Тестирование** - изоляция тестов друг от друга (Jest использует vm для изоляции модулей)
  • **Config-as-Code** - безопасное выполнение конфигурационных файлов (.js configs вместо JSON)

**vm ≠ безопасность!** Код в VM может: - Зациклиться (DoS атака) - Сожрать всю память (нет лимита RAM) - Escape через прототипы (`constructor.constructor('return this')()`) - Использовать CPU на 100% Для реальной безопасности нужен **isolated-vm** или **Worker Threads**.

Почему vm.runInNewContext безопаснее eval, но не является полной песочницей?

Contexts: createContext и runInContext

**Context** - это V8 окружение с собственным глобальным объектом. Контекст можно создать один раз и переиспользовать для многих скриптов - это быстрее, чем каждый раз создавать новый.

API для работы с контекстами

МетодКонтекстКогда использовать
vm.runInThisContext(code)Текущий globalКомпиляция без изоляции (как eval, но без локального scope)
vm.runInNewContext(code, sandbox)Создает новый каждый разОдноразовое выполнение (медленно)
vm.runInContext(code, context)Переиспользует существующийМногократное выполнение (быстро)

**Оптимизация:** Если выполняете один и тот же скрипт много раз, скомпилируйте его в `vm.Script` - V8 закеширует байт-код.

**Частая ошибка:** Забыть передать `console` в контекст. Без этого `console.log` будет `undefined` внутри VM.

В чем преимущество vm.Script над vm.runInNewContext?

Создание безопасной песочницы

Безопасная песочница требует не только изоляции scope, но и контроля ресурсов: **времени выполнения**, **памяти**, **доступа к API**. VM предоставляет базовые инструменты, но не защищает от всех атак.

Ограничение ресурсов

**Проблема:** `timeout` работает только для CPU-bound циклов. Если код делает `await` или `setTimeout`, таймаут не сработает (они асинхронные, control flow выходит из VM).

Ограничение доступа к API

**Совет:** Используйте `Object.freeze()` на переданных объектах, чтобы код не мог их изменить: ```typescript const sandbox = { Math: Object.freeze(Math) }; ```

Proxy для контроля доступа

Use Case: Plugin System

Если я не передаю require в sandbox, код не сможет его получить

Код может escape через constructor.constructor('return this')() или Object.getPrototypeOf

VM изолирует только глобальный объект, но прототипы (Function, Object) остаются общими. Через них можно получить доступ к реальному global и require.

Какой из этих методов НЕ защищает от DoS-атаки через бесконечный цикл?

ESM модули в VM: vm.Module

**vm.Module** (Node.js 13+) позволяет выполнять ES модули (`import/export`) в изолированном контексте. Это нужно для dynamic imports, module mocking в тестах, или загрузки пользовательских модулей.

**Важно:** vm.Module - это низкоуровневое API. Для обычных задач используйте динамические `import()` или мокирование через Jest/Vitest.

Создание синтетического модуля

Загрузка ESM из строки

importModuleDynamically: резолвинг зависимостей

**Ограничение:** vm.Module не поддерживает CommonJS (`require`). Для этого нужно реализовать свой module loader или использовать библиотеки типа `module-compiler`.

Use Case: Hot Module Reload

В чем разница между vm.SyntheticModule и vm.SourceTextModule?

Уязвимости VM и альтернативы

**VM не является песочницей безопасности.** Существуют известные уязвимости, которые позволяют выйти из изолированного контекста и получить доступ к `require`, `process`, файловой системе.

Атака 1: Prototype Pollution

Атака 2: Constructor Escape

**Проблема:** Даже если удалите `Function`, его можно получить через `({}).constructor.constructor`. Нужно патчить все прототипы - это сложно и хрупко.

Атака 3: Timing Attack (DoS)

Решение: isolated-vm

**isolated-vm** - библиотека от Figma, которая создает отдельный V8 Isolate (как отдельный процесс V8) с полной изоляцией памяти и контролем ресурсов.

РешениеИзоляцияЛимитыПроизводительность
vm.runInContextScope толькоtimeout (частично)Быстро
isolated-vmПолная (отдельный V8)CPU, RAM, timeoutСредне (копирование данных)
Worker ThreadsОтдельный потокterminate()Средне (копирование данных)
Child ProcessОтдельный процессkill()Медленно (IPC)

**Когда использовать что:** - **vm** - для trusted код (шаблоны, конфиги, REPL) - **isolated-vm** - для untrusted код (плагины, user scripts) - **Worker Threads** - для CPU-тяжелых задач - **Child Process** - для максимальной изоляции (Docker-in-Docker)

Best Practices

  1. **Никогда не использовать vm для untrusted кода** - только для trusted или limited-trust (внутренние плагины)
  2. **Whitelist API** - передавайте только необходимые функции, не весь `global`
  3. **Object.freeze()** - заморозьте все переданные объекты и прототипы
  4. **Timeout + memory monitoring** - следите за `process.memoryUsage()` и убивайте долгие скрипты
  5. **Content Security Policy** - если это веб-контекст, используйте CSP для блокировки eval
  6. **Code review** - проверяйте пользовательский код перед выполнением (статический анализ)
  7. **Rate limiting** - ограничивайте частоту выполнения (не более N скриптов в минуту)

Если я использую vm.runInContext с timeout, мой сервер защищен от DoS

Timeout защищает только от CPU-bound циклов, но не от async рекурсии, memory leaks, или prototype pollution

VM - это логическая изоляция scope, а не ресурсов. Для реальной защиты нужны: 1. **isolated-vm** или **Worker Threads** для изоляции памяти 2. **process.memoryUsage()** мониторинг 3. **Rate limiting** на уровне приложения 4. **Статический анализ** кода перед выполнением

Ключевые идеи

  • **VM ≠ безопасность:** изолирует scope, но не защищает от DoS, memory leaks, prototype pollution
  • **vm.createContext()** быстрее, чем vm.runInNewContext() - переиспользуйте контексты
  • **vm.Script** кеширует байт-код - используйте для многократного выполнения одного скрипта
  • **vm.Module** поддерживает ESM (import/export), но не CommonJS (require)
  • **isolated-vm** - единственное production-ready решение для untrusted кода (отдельный V8 Isolate)

Связанные темы

VM Module - часть экосистемы изоляции и параллелизма в Node.js:

  • Worker Threads — Альтернатива VM для CPU-тяжелых задач - отдельный поток с MessagePort для обмена данными
  • Child Processes — Максимальная изоляция через fork() - отдельная память и процесс ОС, но медленный IPC
  • Cluster Module — Load balancing через форки - каждый worker в своем процессе, но без изоляции кода

Вопросы для размышления

  • Какие части типичного приложения могут выполнять untrusted код? Конфиги, плагины, шаблоны?
  • Если в коде встречается eval() или new Function(), можно ли заменить их на VM для большей безопасности?
  • Для каких задач достаточно vm.runInContext, а где нужен isolated-vm или Worker Threads?

Связанные уроки

  • os-05-sync
VM Module: Песочница и изоляция

0

1

Войти

Почему isolated-vm безопаснее, чем vm.runInContext?