Error when trying to use isolated-vm in a Forge App

We are trying to build a Forge app that allows users to upload their own merge check as a script. For security reasons, we wanted to use isolated-vm as it offers a greater degree of security than the built-in Node vm. However, when trying to deploy the Forge app, we run into the following:

Error: Bundling failed: Module not found: Error: Can't resolve './out/isolated_vm'

I can confirm that the directory is there and the issue seems to be specific to Forge. Could it be something to do with Forge’s own isolation? Is what we are trying to do redundant? It was not clear to me that Forge prevents interaction between different components within the same tenant nor that it handles malicious code or vulnerabilities.

Runtime V1 was based on isolated-vm and we moved away from it for a number of reasons. Runtime V2 is even more secure and performant. I’ll try and see if somebody from the team could give a better rundown but it would be best for you to list the additional security requirements you are hoping to achieve so that the team can address that directly.

1 Like

Hey @MattGudenas, thanks for reaching out!

It might be the case that RuntimeV2 which is currently in Preview, soon to be GA, better suits your use-case and provides you with more flexibility and security. If you wanted to explore more you can find more information here.

Otherwise, if you could provide more information around the additional security you are trying to provide and what the Forge platform is not providing, we could comment further. Generally though, Forge functions run in isolation to each other on AWS’s Lambda platform providing isolation from both other components and the tenant itself. This isolation strives to protect both developers, customers and tenants from malicious code and vulnerabilities.

2 Likes

According to the shared responsibility model, we are responsible for data protection and preventing vulnerabilities. If our users upload their own merge script, they could be doing things in it that would inadvertently (or deliberately) lead to security issues or data loss. This is why we are looking for a way to isolate the user-uploaded scripts from the main app, or another layer of isolation within the app bundle, if you like.

@MatthewFreeman we just tested something this morning and we have a more concrete example to share.

Suppose we have a Forge app that allows a user to write and execute their own script. If one tenant modifies a global variable, it will then be modified for all tenants. This is not something we can allow and it is critical for us to be able to isolate the runtimes of user-created scripts. I hope this helps.

1 Like

Hey @MattGudenas!

I’m not sure I follow your example. I agree that if not careful, a uploaded script might be able to access environment variables used in your app, though I don’t think that this would impact other tenants. Your app’s invocation(s) run in isolation from both the tenant and other installations of your app. App data (excluding any storage data your app might be using) is passed to the invocation of the app in the AWS Lambda. This data that is passed through, including environment variables and invocation information, is a copy of any data that you have configured. Changing it will only change it for that invocation of your app.

If I understand your example correctly, each invocation of your app isolates BOTH the runtime and any uploaded scripts together. There shouldn’t be any possibility of editing global variables for your app that is persisted beyond the single invocation for that script.

If I haven’t understood your example, did you have an example script/app that reproduces your concerns so that I may better understand?

Thanks!

1 Like

Ok sure, I will provide more detail. Here is the code. First of all, we have the UI, which is a simple text area that allows the user to input their script. You can imagine that this will be replaced by a code editor like Monaco:

import ForgeReconciler, {Button, Form, FormFooter, FormSection, Label, useForm, TextArea} from '@forge/react';
import { invoke } from '@forge/bridge';
import React from 'react';
const App = () => {
    const { handleSubmit, register, getFieldId } = useForm();
    const onSubmit = (formData: any) => {
        const query: any = {}
        query["code"] = formData.script
        invoke("executeScript", query).then((res) => console.log(res));
    };
  return (
 <>
    <Form onSubmit={handleSubmit(onSubmit)}>
        <FormSection>
            <Label labelFor={getFieldId("script")}>
                Script
            </Label>
            <TextArea {...register("script", {required: true})} />
        </FormSection>
        <FormFooter>
            <Button appearance="primary" type="submit">
                Run Script
            </Button>
        </FormFooter>
    </Form>
 </>
  );
};
ForgeReconciler.render(
  <React.Fragment>
    <App />
  </React.Fragment>
);

We also have a resolver that executes the script in a VM:

import Resolver from '@forge/resolver';
import { ResolverRequest } from '../types';
const resolver = new Resolver();
const abc = 25;
resolver.define("executeScript", async function(req) {
  const { payload } = req as ResolverRequest
  if (payload.code != undefined) {
    try {
      console.log(abc)
      const vm = require('vm')
      const script = new vm.Script(`${payload.code}`)
      const result = script.runInThisContext()
      console.log(result)
      return result
    }
    catch (e) {
      return "Script threw an exception: " + e
    }
  }
  return "Unable to execute code or no code provided";
});
export const executeCodeHandler = resolver.getDefinitions();

So, abc is a variable that is defined outside of the user-uploaded script (a global variable). Now, suppose we have two different tenants (A and B) that both install this app in their own workspaces. Now, if tenant A were to enter global.abc = 10 into their script window (the frontend), they would set abc to 10 for BOTH tenant A and tenant B, even though they are installed on different workspaces.

This has been verified by multiple engineers and at the moment our theory is that this is because the app id in the manifest is the same for both apps (as it is hardcoded), so they are treated as the same application, but we are not sure.

https://developer.atlassian.com/platform/forge/runtime-reference/#developer-responsibilities states that “Your app must not persist customer data or sensitive content in global state, in memory or on disk, between subsequent invocations.”
This implies at least that it is possible to share data between different tenants.