Vm2: Not immune to while(1){}

Created on 2 Jan 2019  ·  19Comments  ·  Source: patriksimek/vm2

Following code will not time out.

const {VM} = require('vm2');
new VM({timeout:1}).run(`
        function main(){
        while(1){}
    }
    new Proxy({}, {
        getPrototypeOf(t){
            global.main();
        }
    })`);
bug confirmed help wanted

All 19 comments

There is likely no good solution to this. NodeJS is single process. while(1){} protection requires multi-process so that the checker does not get blocked by the while(1){}. Maybe if we could rekindle the SpiderNode project.......

This is caused by Decontextify.value testing for the guest object's prototype with instanceOf. Node.js vm module is of course immune to this. A poor man's patch might be to backdoor Proxy so that we could detect any Proxy objects from the sandbox, but an ideal approach would be to just never trust any objects and functions from it and put contextify.js into sandbox, using primitive values to communicate and handles for referencing guest objects.

Just for folks to see, browserless uses a child-process and message-passing to talk with the child running the untrusted code. It's a bit of a setup task, but once done you'll be immune to while(1){} attacks. The source is here.

The whole point of this project seems to be to not spawn a whole separate
OS process. Otherwise there are many solutions available which are much
more secure.

Otherwise there are many solutions available which are much more secure.

Which ones? I'm not sure there's a better alternative to VM2 in NodeJS, at least not that I've seen

The maintainer of this library pointed to this alternative in issue #80:

https://github.com/laverdet/isolated-vm

I haven't tried it yet myself.

I only run untrusted code in child process. Can't risk running it in the main V8 instance especially when you know it's single threaded.

The problem of calling V8 engine directly is lack of importing module. In my case I need to import request module to allow HTTP operations. It also has to be asynchronous.

I find vm2 + child_process a good combination.

Hey,

myself and @harelmo have created a library on top of vm2 that is immune to while(1) {}.

Similar to what tian-ma mentioned, but we use node.js worker threads instead of child processes.

take a look here: https://www.npmjs.com/package/isolated-runtime and let us know what you think :)

Another workaround would be to just disable Proxy within the sandbox, right?

const vm = new VM({
    sandbox: {
        Proxy: undefined
    },
    timeout: 100
});

Using Promise it is also possible to write code that never times out:

"use strict";
const {VM} = require('vm2');
const untrusted = '(' + function(){
    Promise.resolve().then(a=>{
        while(1){}
    });
}+')()';
try{
    console.log(new VM({timeout:10}).run(untrusted));
}catch(x){
    console.log(x);
}

Problem here is that .then will queue the function to be called later in the microtask queue.
To fix this we need to write our own Promise.then & related functions or disable Promise.

Also this works:

const {VM} = require('vm2');

const script = '(' + function() {
    (async function x(){
        await {then: y=>y()};
        while(1){}
    })();
}+')()';

new VM({timeout:10}).run(script);

So just overriding Promise won't work.
No idea how to fix this.

While this remains open, we should add a clarification to the README to warn potential users. This vulnerability allows one to escape the sandbox and thus it defeats the purpose of the library.

Readme was updated https://github.com/patriksimek/vm2/commit/77f5265ab53b87864a312156ee62b1082787e9b0#diff-04c6e90faac2675aa89e2176d2eec7d8. And I don't know how this could be used to escape the sandbox, you only can escape the intended timeframe the script should run in.

Perhaps this should be its own issue, but why does NodeVM not support "timeout"?

@crowder since in NodeVM the setTimeout, setInterval & setImmediate exists, it would be easy to circumvent the timeout. Maybe there are other reasons too, but I don't work with NodeVM, only VM.

@XmiliaH wouldn't implementing a timeout prevent at least from unintended infinite loops?
The malicious while(true) + setTimeout combination would still be an issue but any simple unintended infinite loop done by mistake could be handled by supporting timeout

My concern is that NodeVM allowes to require modules which might be host modules and timing out while they change global state might be bad. Also the NodeVM returns a module and users quite likely would call functions exposed by the module, however the timeout would only apply to the module loading and not the function calls.
If you want to call a function with a timeout you can just implement it yourself like

function doWithTimeout(fn, timeout) {
    let ctx = CACHE.timeoutContext;
    let script = CACHE.timeoutScript;
    if (!ctx) {
        CACHE.timeoutContext = ctx = vm.createContext();
        CACHE.timeoutScript = script = new vm.Script('fn()', {
            filename: 'timeout_bridge.js',
            displayErrors: false
        });
    }
    ctx.fn = fn;
    try {
        return script.runInContext(ctx, {
            displayErrors: false,
            timeout
        });
    } finally {
        ctx.fn = null;
    }
}

Good point @XmiliaH and thanks for the example
I see your example is using vm package and not vm2
Would be nice to be able to use vm2 and still be able to timeout it out

@szydan The code I posted is used by VM2 to timeout the VM. If the NodeVM had a timeout too, it would use the same functionality.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

wojpawlik picture wojpawlik  ·  4Comments

CapacitorSet picture CapacitorSet  ·  13Comments

somebody1234 picture somebody1234  ·  4Comments

KonradLinkowski picture KonradLinkowski  ·  10Comments

vshymanskyy picture vshymanskyy  ·  8Comments