laverdet / isolated-vm

Secure & isolated JS environments for nodejs
ISC License
2.12k stars 148 forks source link

Running Untrusted Vento/Nunjucks Templates in isolated-vm #495

Open ErikTheBerik opened 4 days ago

ErikTheBerik commented 4 days ago

I'm exploring using isolated-vm to safely execute untrusted VentoJS templates, which is a template engine similar to Nunjucks but can run arbitrary JavaScript. My goal is to isolate the JavaScript running within the Vento templates, restricting it to the isolated-vm environment while exposing only a controlled set of functions and variables.

Here is a very simple example of running some malicious ventojs template that will output the contents of .env:


import vento from "ventojs";

async function RunMaliciousTemplate() 
{
    const env = vento();

    const envAccessTemplate = `
     {{ import env from ".env" }}
    .env file contents: {{ JSON.stringify(env) }}
    `;

    const result = await env.runString(envAccessTemplate);
    console.log(result.content);
}

RunMaliciousTemplate();

What I'm Trying to Achieve:

  1. VentoJS Environment Setup: I'd like to create a VentoJS environment where I define a set of safe functions and variables that can be accessed by the template logic.
  2. Context Isolation: The execution of the template must be confined to the isolated-vm context. This means restricting the JavaScript in the templates to only the exposed APIs, preventing unauthorized access to modules such as fs, fetch, or others that may pose security risks.
  3. Controlled Access to External Functions: While I want to block dangerous operations (like file reads), I still need to provide controlled access to external functions (e.g., functions that perform fetch operations), though these should be executed outside of the isolated context to avoid breaking isolation.

My Current Approach:

Questions/Concerns:

Best Approach to Passing Modules: I understand that passing modules to the isolated context (such as VentoJS) is a common challenge, I have done some initial research and would like some guidance. Is it necessary to package the entire ventojs module into a single file (as mentioned by Issue #124 ), or is there a simpler way to achieve this isolation? Handling External I/O: Since I want to block direct file access but still allow certain functions (like network calls) outside the isolated environment, should I rely on wrapping functions in external contexts? Is there an example or pattern you’d recommend for handling these use cases securely?

Additional Notes:

I have no need to run untrusted JavaScript that interacts with the filesystem directly (e.g., no fs or file reads in the isolated-vm context). I’m hoping to avoid reinventing the wheel and would appreciate any guidance on best practices or existing solutions for isolating template engines like VentoJS or Nunjucks. I know that I'm not the first one to try this.

Any advice on how to properly set this up with isolated-vm, or examples of similar implementations, would be incredibly helpful!

laverdet commented 4 days ago

Bundling the javascript is the easiest way to get it all into isolated-vm

ErikTheBerik commented 4 days ago

I'll attempt to bundle Vento.js into a single file using Rollup and integrate it with isolated-vm. I was hoping for an easier solution, but this seems like the best approach. If I encounter any issues, I'll update this thread; otherwise, I'll close the issue and share any relevant insights to assist others facing a similar situation in the future.

laverdet commented 4 days ago

It really helps to think of isolated-vm like a web browser. How would you get ventojs into a web browser? You would bundle it. It just so happens that the tooling for web browsers is more developed so most people don't even think about it anymore.

ErikTheBerik commented 4 days ago

That analogy makes a lot of sense. In terms of a browser bundling would be the logic approach, so I see why I should do the same in isolated-vm.

I have a question about file systems. As mentioned before, the Vento.js template I created outputs the contents of the .env file. If I bundle the full Vento.js module into isolated-vm, will isolated-vm still be able to fetch the contents of the .env file? Additionally, will it retain access to the same files that Node.js has access to?

If I think of it in terms of a browser, as long as the browser has file access, it can retrieve files from the host's computer. In this case, the host would be the server. But I assume isolated-vm doesn't actually have access to the filesystem, right?

laverdet commented 4 days ago

JavaScript, the language, does not define any filesystem operations. isolated-vm is a plain JavaScript environment.

ErikTheBerik commented 4 days ago

Ah, of course! If I want to access the contents of a file, I'd need to use a module like fs. So, as long as I don't bundle such a module with Vento.js, attempting to fetch the contents of a file should simply fail—which is exactly the behavior I'm aiming for.