· updated
What runs when you import someone else's collection
A collection looks like data: URLs, headers, request bodies.
Sitting next to them in the same file are pre-request and post-response scripts — ordinary JavaScript that your client will run on your machine. Sign a request or refresh a token, pull an id out of a response into a variable. Useful things; I write them every week.
They run silently. You hit Send, and the script has already finished. Of the clients I know, exactly one asks whether you want that to happen.
Which raises a question worth asking before you open a collection from someone else's repository: what can that script actually do to your machine? The answers differ more than you would expect from four tools that look alike.
Postman: strip out everything dangerous
Scripts run in a Node-based sandbox. Dangerous globals are stripped from the context before the script starts, child_process, fs and os are blocked, and an allowlist of libraries goes in: lodash, crypto-js, chai, moment, uuid and about a dozen more.
The protection holds on one condition: that the developers found every path to an object from outside.
In 2024, Sonar showed they had not. setTimeout returned a timer object created outside the sandbox; walking its prototype chain, researchers reached the Function constructor, and through it require and the system modules. Postman closed the path on 3 April 2024 in version 10.24.16 by hiding those timer objects. A patch on one route, not on the class of routes.
Node's own documentation is blunt about the mechanism:
vmis not a security mechanism
What it costs you. Your safety equals the completeness of somebody else's deny list, and you cannot audit it: the sandbox code lives inside a proprietary package.
Insomnia: hand out modules from a list
Insomnia is the opposite pole. Scripts run in a hidden Electron window, and require is still there, allowed for the built-in path, assert, buffer, util, url and stream, with chai, cheerio, crypto-js, lodash, moment and uuid on top.
Of the four approaches this is the most comfortable one to use: a script from Postman's documentation ports over almost unchanged, and crypto-js for signing is already in place.
The history matches the approach. Unit tests once ran with no isolation at all, and one line was enough:
require("child_process").execSync("id > /tmp/pwnd")
Two more routes turned up beside it. Nunjucks templates could reach child_process through a function constructor, and pre-request scripts had fs, enough to append a line to ~/.bashrc and wait for you to open a terminal. The tests were isolated, the templates fixed, fs taken away from scripts.
What it costs you. How safe you are comes down to what is on the list you were handed, and that list grows with every release. Every module on it is surface somebody has to keep checking.
Bruno: real isolation with an off switch
Bruno started on vm2, a library its author declared dead in July 2023, saying the problem could not be fixed properly. Escaping took five lines: through the prototype chain a script reached process, then require, then a shell command.
Variable values were a second storyline. They were interpolated as JavaScript template strings, which means the contents of a variable ran as code, bypassing the sandbox entirely.
What Bruno did next was the most drastic move of the four. They took QuickJS, a different JavaScript engine altogether, and compiled it to WebAssembly. Collection code now runs inside Node's wasm interpreter, which has no require and cannot touch the filesystem or the network. Bruno calls this Safe Mode.
Next to it sits Developer Mode on NodeVM with full system access, and Bruno asks which one you want when you open a collection. That is the one client that asks.
What it costs you. The decision is handed to you. When a script does not work in Safe Mode, flipping to Developer Mode is faster than finding out why. From then on the collection has full access to your system, with the choice made permanently.
Hoppscotch: two mechanisms in one product
In the web and desktop apps, scripts run in a Web Worker — a separate thread with no access to Node.
The CLI, the one people install in CI, ran on the same old vm. To let scripts read environment variables, references to outside objects were passed in. One of them was the way out: arbitrary commands on whatever machine runs hoppscotch-cli. That is CVE-2024-34347, CVSS 8.3. Afterwards the CLI moved to isolated-vm, which gives it a context of its own at the interpreter level.
What it costs you. "Safe" depends on where the same collection runs. A Web Worker in the app, a different mechanism in CI, each with its own history.
Across the four, a pattern shows. Where the sandbox lives in the same engine as the application, isolation comes down to never handing the script a single reference outward: the developer has to enumerate all of them, an attacker needs one that was missed. With a separate engine there is nothing to enumerate.
How long that race runs is visible in vm2. On 27 January 2026 another bypass turned up in the current version — CVE-2026-22709, CVSS 9.8, reaching child_process through an unhandled promise. The library was pronounced dead back in 2023, then revived, and it is still being patched as new escapes appear.
What we picked, and why
Our client is written in Go, which changes the problem. The world where the script runs can be built empty, with no host objects to hide in the first place.
We use goja, a JavaScript interpreter written in Go: a standalone implementation of the language rather than a V8 binding or a Node fork. It has no module system and no access to the filesystem or the network, and nothing can carry those in. Go has no require, and the bridge between Go and JavaScript is one we build by hand, one object at a time.
So the escape that worked on Postman runs into nothing here:
this.constructor.constructor('return process')()
// ReferenceError: process is not defined
process is not hidden from the script; it simply does not exist in this interpreter. A script gets standard ECMAScript, console, and our pm object — 58 names in total, with no require, Buffer, fetch or setTimeout among them.
All 58 names
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
The build mattered too. goja is pure Go, so the client builds with one go build and no C toolchain, and the download stays at 29 MB.
What that isolation costs is collections that need npm. A script calling require('crypto-js') will not run, and there is nothing to rewrite it with yet: the sandbox has no crypto, and no btoa either. There is no chai either, so assertions inside pm.test are written by throwing:
pm.test("token came back", function () {
if (!pm.response.json().access_token) throw new Error("no access_token")
})
In exchange, every request gets a fresh virtual machine, and that costs 22–30 µs per run including the machine itself.
What the sandbox holds
Measured by running the scripts through the same engine that runs user scripts.
| Probe | Result |
|---|---|
typeof require, process, Buffer, module | undefined |
Function('return process')() | ReferenceError: process is not defined |
typeof fetch, XMLHttpRequest, WebSocket | undefined |
while (true) {} | interrupted at 5.00 s |
Infinite loop inside try/catch | interrupted at 5.00 s; the interrupt is not swallowed |
| Infinite recursion | interrupted by the timeout |
/(a+)+$/.test("a".repeat(40) + "!") | 0 s |
globalThis.__leak from a previous run | undefined |
The second-to-last row is a happy accident: the classic ReDoS pattern that takes services down compiles to a Go regexp here, and that one runs in linear time. You can run the whole table yourself with our harness, a plain Go test.
Test your own client
Paste this into a pre-request script in any API client, ours included. Two lines that show what a script can do to your machine:
console.log(typeof require, typeof process) // anything but undefined is a bad sign
console.log(typeof fetch, typeof XMLHttpRequest) // network around your proxy and logs
And two more for what it can do to the client itself. Run these one at a time, with your work saved. The client is allowed not to survive them.
while (true) {} // expect an error, not a freeze
var a = []; while (true) { a.push("x".repeat(1024)) } // how much memory it will hand over
"It froze and I had to kill the process" is an answer too.
An imported collection is someone else's code running on your machine. Treat it the way you treat a package before npm install.
Links
- Sonar, "Scripting Outside the Box: API Client Security Risks", part 1 and part 2
- CVE-2024-34347 — sandbox escape in the Hoppscotch CLI
- CVE-2026-22709 — January's vm2 escape
- goja — a JavaScript interpreter in Go
- Our measurement harness and the sandbox regression tests