laverdet / isolated-vm

Secure & isolated JS environments for nodejs
ISC License
2.2k stars 154 forks source link

Accessing Readable dynamically. #403

Closed Valanap closed 1 year ago

Valanap commented 1 year ago

While we are migrating from vm2 (like many others), where this use case worked as expected (there was direct access to object with reference), we cannot replicate Readable functionality in isolated-vm, I add the comment that the code is executed only in our separate NodeJS application.

Assuming that user can:

We tried multiple options including unsafe ref.

I have created minimal repository, in real world, the readable function inside file is the readable from different SDK.

import ivm from 'isolated-vm';

async function init() {
  const isolate = new ivm.Isolate();
  const context = await isolate.createContext();

  const files = [{
    fileId: '123456789',
    fileName: 'example.txt',
    readable: () => {
        return new Readable({
            read() { this.push('hello'); this.push(null);}
        });
    }
}]

  await context.global.set('___getFile', new ivm.Reference(async (fileId) => {
    const fakeDbLookup = await new Promise(res => { res(files.find(it => it.fileId === fileId))});
    return new ivm.ExternalCopy(fakeDbLookup).copyInto();
  }));

  await context.global.set('log', (...args) => {
    console.log(...args);
  })

  const script = await isolate.compileScript(`
    const getFile = async (fileName) => {
        return ___getFile.apply(undefined, [fileName], { arguments: { copy: true }, result: { promise: true }});
    }

    ( async() => {
        try {
            const myFile = await getFile('123456789');
            const readStream = myFile.readable();

            const data = [];

            await new Promise(res => {
                readStream.on('data', (chunk) => {
                    data.push(chunk);
                  });

                readStream.on('end', () => {
                    log('Stream complete.');
                    res();
                });
            });

            log(data.join(','));

        } catch(e) {
            log(e);
        }
    })();
  `);
  await script.run(context);
};

init();

Provided example shows:

  await context.global.set('___getFile', new ivm.Reference(async (fileId) => {
    const fakeDbLookup = await new Promise(res => { res(files.find(it => it.fileId === fileId))});
    console.log(fakeDbLookup);
    /*
    {
        fileId: '123456789',
        fileName: 'example.txt',
        readable: [Function: readable]
    }
    */
    return new ivm.ExternalCopy(fakeDbLookup).copyInto(); // readable() => could not be cloned
  }));

Created different sample with unsafeInherit, but also doesn't work, the Reference seem to be empty that way.

  await context.global.set('___getFile', new ivm.Reference(async (fileId) => {
    const fakeDbLookup = await new Promise(res => { res(files.find(it => it.fileId === fileId))});
    return fakeDbLookup; // myFile.readable is not a function since Reference {}
  }, { unsafeInherit: true}));

Looking for community help on how to resolve this issue, to get access to the Readable created dynamically with resolved promise.

laverdet commented 1 year ago

You can't access a readable from the isolate. Think of the isolate like Safari. How would you access a readable that you made in nodejs from safari? It's non-sense. You need to set up a system of callbacks to pass data back an forth.