· обновлено
Что выполняется, когда вы импортируете чужую коллекцию
Коллекция выглядит как данные: URL, заголовки, тело запроса.
Вместе с ними в файле лежат pre-request и post-response скрипты — обычный JavaScript, который ваш клиент выполнит на вашей машине. Подписать запрос, обновить токен, вытащить id из ответа в переменную. Вещь полезная, я пишу такие каждую неделю.
Запускаются они молча. Нажали «Send» — скрипт уже отработал. Из известных мне клиентов «разрешить выполнение?» спрашивает ровно один.
Отсюда вопрос, который стоит задать до того, как открыть коллекцию из чужого репозитория: а что этот скрипт вообще может сделать с вашей машиной? Ответы расходятся сильнее, чем ждёшь от четырёх однотипных инструментов.
Postman: вычистить всё опасное
Скрипты выполняются в песочнице на базе Node. Перед запуском из неё вычищают опасные глобальные объекты, child_process, fs и os блокируют, а внутрь кладут белый список библиотек: lodash, crypto-js, chai, moment, uuid и ещё десяток.
Защита работает при одном условии: разработчики нашли все способы дотянуться до объекта снаружи.
В 2024 году Sonar показал, что нашли не все. setTimeout возвращал объект таймера, созданный вне песочницы; по цепочке его прототипов исследователи добрались до конструктора Function, а через него — до require и системных модулей. Postman закрыл проход 3 апреля 2024 в версии 10.24.16: спрятал те самые объекты таймера. От других проходов того же класса эта заплата не защищает.
Документация Node на этот счёт высказывается прямо:
vmis not a security mechanism
Чем платит пользователь. Безопасность держится на том, насколько полно разработчики собрали список запретов. Проверить её самому нельзя: код песочницы внутри проприетарного пакета, и что там сегодня — вы не знаете.
Insomnia: раздать модули по списку
Другой полюс. Скрипты идут в скрытом окне Electron, и require там не убран — он разрешён для встроенных модулей path, assert, buffer, util, url, stream, а сверху лежат chai, cheerio, crypto-js, lodash, moment, uuid.
Из четырёх подходов этот для пользователя самый удобный: скрипт из документации Postman переносится почти без правок, а crypto-js для подписи запроса уже на месте.
История у подхода соответствующая: unit-тесты когда-то запускались без изоляции вообще, и хватало одной строки:
require("child_process").execSync("id > /tmp/pwnd")
Рядом нашлись ещё два прохода. Шаблоны на Nunjucks позволяли добраться до child_process через конструктор функции, а pre-request скриптам был доступен fs — этого достаточно, чтобы дописать строку в ~/.bashrc и подождать, пока вы откроете терминал. Тесты изолировали, шаблоны починили, fs из скриптов убрали.
Чем платит пользователь. Вся защита здесь — выданный список, а он растёт с каждой версией. Каждый модуль в нём — поверхность атаки, которую кто-то должен проверять.
Bruno: правильная изоляция с выключателем
Bruno начинал с vm2 — библиотеки, которую автор объявил мёртвой в июле 2023 со словами, что проблему нельзя закрыть нормально. Побег оттуда занимал пять строк: через цепочку прототипов скрипт получал process, оттуда — require, оттуда — системную команду.
Вторым сюжетом шли значения переменных: их подставляли как шаблонные строки JavaScript, а значит, содержимое переменной исполнялось как код — в обход песочницы.
Дальше Bruno сделал самый радикальный шаг из четырёх. Взял QuickJS — другой движок JavaScript целиком — и скомпилировал в WebAssembly. Код из коллекции гоняет интерпретатор wasm внутри Node: ни require, ни файловой системы, ни сети. Это Safe Mode.
Рядом с ним есть Developer Mode на NodeVM с полным доступом к системе, и при открытии коллекции Bruno спрашивает, какой выбрать, — тот самый вопрос «разрешить выполнение?», которого нет у остальных троих.
Чем платит пользователь. Решение переложено на него. Если скрипт не работает в Safe Mode, включить Developer Mode быстрее, чем разбираться в причине, — а дальше коллекция получает полный доступ к системе, и выбор этот сделан навсегда.
Hoppscotch: два разных механизма в одном продукте
В вебе и десктопе скрипты гоняет Web Worker — отдельный поток без доступа к Node.
А в CLI, который ставят в CI, стоял всё тот же vm. Чтобы скрипт видел переменные окружения, внутрь передавали ссылки на внешние объекты. Через одну из них из песочницы и вышли — произвольные команды на машине, где запущен hoppscotch-cli. Это CVE-2024-34347, CVSS 8.3. После него CLI перевели на isolated-vm — свой контекст на уровне интерпретатора.
Чем платит пользователь. «Безопасно» здесь зависит от того, где запущена одна и та же коллекция: в приложении — Web Worker, в CI — другой механизм, и своя история у каждого.
Из четырёх историй видна закономерность. Пока песочница живёт в том же движке, что и приложение, изоляция сводится к тому, чтобы не отдать скрипту ни одной ссылки наружу: разработчик обязан перечислить все, атакующему хватит одной пропущенной. У отдельного движка такого списка просто нет.
Насколько это долгая гонка, видно по vm2. 27 января 2026 года в актуальной версии нашли очередной обход — CVE-2026-22709, CVSS 9.8, выход к child_process через необработанный промис. Библиотека была объявлена мёртвой ещё в 2023-м, потом воскрешена, и её продолжают чинить по мере находок.
Что выбрали мы и почему
Наш клиент написан на Go, и это меняет саму постановку задачи. Не «спрятать хост-объекты от скрипта», а не иметь их в том мире, где скрипт выполняется.
Мы взяли goja — интерпретатор JavaScript, написанный на Go: самостоятельную реализацию языка, а не обёртку над V8 или форк Node. Модулей, файловой системы и сети у неё нет, и принести их туда некому: в Go нет require, а мост между Go и JavaScript мы строим руками, по одному объекту.
Поэтому побег, сработавший у Postman, здесь упирается в пустоту:
this.constructor.constructor('return process')()
// ReferenceError: process is not defined
process не спрятан от скрипта. Его там нет. Скрипту доступен стандартный ECMAScript, console и наш объект pm — ровно 58 имён, и в этом списке нет ни require, ни Buffer, ни fetch, ни setTimeout.
Все 58 имён
AggregateError, Array, ArrayBuffer, BigInt, BigInt64Array, BigUint64Array, Boolean,
DataView, Date, Error, EvalError, Float32Array, Float64Array, Function, GoError,
Infinity, Int16Array, Int32Array, Int8Array, JSON, Map, Math, NaN, Number, Object,
Promise, Proxy, RangeError, ReferenceError, Reflect, RegExp, Set, String, Symbol,
SyntaxError, TypeError, URIError, Uint16Array, Uint32Array, Uint8Array,
Uint8ClampedArray, WeakMap, WeakSet, console, decodeURI, decodeURIComponent,
encodeURI, encodeURIComponent, escape, eval, globalThis, isFinite, isNaN,
parseFloat, parseInt, pm, undefined, unescape
Второй довод — сборка: goja не требует нативных зависимостей, поэтому клиент собирается под три платформы одной командой, а дистрибутив остаётся на 29 МБ.
Цена такой изоляции — коллекции, которым нужен npm. Скрипт с require('crypto-js') не поедет, и переписать его пока не на чем: криптографии в песочнице нет, как нет и btoa. Нет и chai — проверки внутри pm.test пишутся через исключение:
pm.test("токен вернулся", function () {
if (!pm.response.json().access_token) throw new Error("нет access_token")
})
Зато каждый запрос получает свежую виртуальную машину, и стоит это 22–30 мкс на прогон вместе с её созданием.
Что песочница держит
Проверяли прогоном через тот же движок, что выполняет пользовательские скрипты.
| Проба | Результат |
|---|---|
typeof require, process, Buffer, module | undefined |
Function('return process')() | ReferenceError: process is not defined |
typeof fetch, XMLHttpRequest, WebSocket | undefined |
while (true) {} | прервано на 5,00 с |
Бесконечный цикл внутри try/catch | прервано на 5,00 с: прерывание не проглатывается |
| Бесконечная рекурсия | прервана таймаутом |
/(a+)+$/.test("a".repeat(40) + "!") | 0 с |
globalThis.__leak из прошлого запуска | undefined |
Предпоследняя строка — приятная случайность: паттерн классического ReDoS, на котором ложатся сервисы, goja компилирует в го-регулярку, а та работает за линейное время. Прогнать всю таблицу у себя можно нашим харнессом — это обычный Go-тест.
Проверьте свой клиент
Вставьте это в pre-request скрипт любого API-клиента, включая наш. Две строки, которые показывают, что скрипт может сделать с вашей машиной:
console.log(typeof require, typeof process) // что-то кроме undefined — плохой знак
console.log(typeof fetch, typeof XMLHttpRequest) // сеть в обход прокси и логов
И ещё две — что он может сделать с самим клиентом. Эти запускайте по одной и на сохранённой работе: клиент имеет право их не пережить.
while (true) {} // ждём ошибку, а не зависание
var a = []; while (true) { a.push("x".repeat(1024)) } // сколько памяти он даст съесть
Ответ «зависло, пришлось убивать процесс» — тоже ответ.
Импортированная коллекция — это чужой код, запущенный на вашей машине, и относиться к ней стоит так же, как к пакету перед npm install.
Ссылки
- Sonar, «Scripting Outside the Box: API Client Security Risks», часть 1 и часть 2
- CVE-2024-34347 — побег из песочницы Hoppscotch CLI
- CVE-2026-22709 — январский побег из vm2
- goja — интерпретатор JavaScript на Go
- Наш харнесс замеров и регрессионные тесты песочницы