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.runInContext | Scope только | 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
- **Никогда не использовать vm для untrusted кода** - только для trusted или limited-trust (внутренние плагины)
- **Whitelist API** - передавайте только необходимые функции, не весь `global`
- **Object.freeze()** - заморозьте все переданные объекты и прототипы
- **Timeout + memory monitoring** - следите за `process.memoryUsage()` и убивайте долгие скрипты
- **Content Security Policy** - если это веб-контекст, используйте CSP для блокировки eval
- **Code review** - проверяйте пользовательский код перед выполнением (статический анализ)
- **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?