In vm2 for versions up to 3.9.19, Node.js custom inspect function allows attackers to escape the sandbox and run arbitrary code.
const {VM} = require("vm2");
const vm = new VM();
const code = `
const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom');
obj = {
[customInspectSymbol]: (depth, opt, inspect) => {
inspect.constructor('return process')().mainModule.require('child_process').execSync('touch pwned');
},
valueOf: undefined,
constructor: undefined,
}
WebAssembly.compileStreaming(obj).catch(()=>{});
`;
vm.run(code);
Node.js allows a custom inspect function to be used instead of the default formatter by defining it as util.custom.inspect
property. This symbol is available cross-realm via Symbol.for('nodejs.util.inspect.custom')
. If inspect()
on an object with a custom inspect function can be triggered within the sandbox, it enables an attacker to leak inspect
function created in host context into the sandbox.
Searching for Node.js internal use cases of inspect()
yields several hits, where we see use cases in determineSpecificType()
@ lib/internal/errors.js. This is reachable by various types of errors, for example ERR_INVALID_ARG_TYPE
and ERR_INVALID_ARG_VALUE
.
Searching for ERR_INVALID_ARG_TYPE
we see this type of error being thrown inside wasmStreamingCallback()
@ lib/internal
/wasm_web_api.js, which is reachable from WebAssembly.compileStreaming()
.
This enables an attacker to leak inspect
function created in host context into the custom inspect function within the sandbox, effectively escaping it.
Note that
ERR_INVALID_ARG_TYPE
andwasmStreamingCallback()
is just an example of triggeringinspect()
with user-controlled object, and using this specific call chain is not necessary to exploit this vulnerability.
Remote Code Execution, assuming the attacker has arbitrary code execution primitive inside the context of vm2 sandbox.
Xion (SeungHyun Lee) of KAIST Hacking Lab
2024-03-30
“Preserve the scene of globalThis and restore when finally”