I recently encountered a situation in the client project, where I was expected to run custom user-provided code, inside my Node environment to transform data. We all know the security implication of opening up our server runtime to external code and if not done correctly, the results are deadly ranging from losing our user’s data to raising our cloud servers bills because of mining injections.
Fortunately, there is a package called vm2 which provides a sandbox for such situations so that we can provide the flexibility for our users and also keep our servers safe. Let us see how it can be configured and used.
vm2 is a sandbox that can run untrusted code with whitelisted Node's built-in modules. Securely!
Some pointers for the sandbox:
- Uses console output of Node process
- Can require external & builtin modules
- Has the ability to setup customized access to builtin modules
- Uses secure context for execution using VM module
- Uses proxies to keep the sandbox code isolated
Let’s get into the implementation,
Here we are using the NodeVM module from vm2 to run the code & we have declared UtilFunctions. If you want to provide a facility to call an API, query database, etc safely, you can just declare functions and make them available to the sandbox. In this way, you won’t have to be worried about providing access to libraries to the sandbox and malicious users exploiting it.
As the functionality is abstracted from the sandbox, all users can do is to call the functions and use the return value if any.
Here we are configuring the NodeVM to get the sandbox. We are disabling eval, wasm, external modules, setTimeout & setInterval. Also, we are not allowing any builtin modules to be used in the code. We are making the sandbox secure and execution swift with this config. Also, we are injecting the UtilFunction into the sandbox.
Here we have declared function _uncaughtException to handle uncaughtExceptions thrown by VM. Function registerListeners will add the listener to VM and function cleanup will remove the listener from VM. This part is just to make sure the Node process won’t exit due to the user entered code.
Moving on to executeJS function where all the magic happens, NodeVM uses callbacks in the sandbox i.e. (after the execution of code a callback will be called by sandbox to convey the result of execution) but usually, we prefer async/await for such functionality as some code will be waiting for the result of user JS execution.
So using promises we made this function async. Now executeJS take 2 arguments code & context. Context is a JSON object which is made available to the user entered JS code, code can make changes to context and after execution, context can be used further. In this way, we make sure that the JS execution sandbox is working as a transformer. Which at the core just modifies the context. But due to this structure for the Node process, the executeJS function is a BlackBox transformer making it better streamlined with your existing system.
Next, we declare func, which is nothing but the user entered code enclosed in a function that is exported. Now we have taken care of exception handling with the exported function itself. As we discussed that VM uses callbacks so we are passing context in the first argument and 2 callbacks to handle success(resolve) & failure(reject) cases. Then we will register listeners to VM. vm.run will return the exported function we formulated in func.
The final step is to call the exported function and passing context & callbacks. In the callbacks, we are calling cleanup to remove listeners from VM and resolving or rejecting the promise accordingly.
Let’s test this,
So, in our test, the code has the user entered code, which
- Checks the context for date and day
- Uses isHoliday UtilFunction to check if its holiday
- Checks for weekend
- Sets plan on the context accordingly or throw an exception
The executeJS function will return modified ctx or throw exception according to data.
Thanks for reading… keep hacking…