Yesterday we found what looked like a critical remote code execution bug in Postman's script sandbox. Two of us reproduced it independently, both times with real command execution. We scored it, wrote most of the report, and started building the end-to-end proof of concept before reaching out to Postman's security team.
Then we ran that proof of concept against the actual, currently published packages, and it failed. Both times.
Here's what happened.
The bug we thought we had
Postman lets you run JavaScript before and after a request, for things like generating auth tokens or checking a response. That script runs in a restricted environment: no process, no require, no filesystem. The restriction works by literally naming those as function parameters and calling the function with undefined for each one, so any script that types process gets undefined instead of the real thing.
There's a classic way around that trick. Function('return this')() compiles a brand new function from a string and calls it, and that new function does not inherit any of the parameter shadowing from the function it was written inside. If it runs in the same JavaScript realm as the real process object, it hands that object straight back to you.
We tried it against a standalone rebuild of Postman's isolation logic (the two open source packages it's built on, uvm and uniscope) and got a real process object back, twice, independently. From there: process.binding('spawn_sync'), and we had command execution.
Where it fell apart
A rebuild of the logic is not the same thing as the actual product. So before writing to Postman, we built the exploit again, this time against the real, currently published postman-sandbox, postman-runtime, and newman packages, called the way an actual script would call them.
Both attempts failed. The error from the newman path was blunt: Cannot read properties of undefined (reading 'mainModule'). The process object we thought we'd recovered was undefined.
It turns out Postman's real sandbox does something our rebuild didn't: before any user script runs, it deliberately strips process, Buffer, and globalThis off the actual global object, a step it calls recreatingTheUniverse(), documented in their changelog since postman-sandbox v4.0.0. On the newman path there's a second, independent layer on top of that: the script runs inside its own separate V8 context, so there is no shared realm for the trick to abuse in the first place.
Our rebuild had faithfully reproduced the parameter-shadowing trick. It just hadn't reproduced the hardening Postman built on top of it.
What we're taking from this
We keep a running rule for ourselves: an exploit only counts as reachable once it's been run against the real, unmodified code through its own public entry point, never against a rebuild of the mechanism, however faithful that rebuild feels. This is the second time we've had to learn a version of that lesson, and this time it cost us a wasted day rather than a wasted submission.
No report went to Postman. Nothing was submitted, nothing was retracted. We're writing this up because the failure is more useful to share than the almost-bug was, and because "we double-checked our own work before it left the building" is the kind of thing you can only prove by actually doing it in public.
Top comments (0)