endo icon indicating copy to clipboard operation
endo copied to clipboard

importHook for node-internal modules?

Open sumbricht opened this issue 2 years ago • 2 comments

For my use case, I require that untrusted code can be run with access to virtually any imported module, except for a few (fs and net would be mocked to only reveal a subset of the functionality). Now I'm struggling a bit with writing an importHook that can correctly load all of the following:

  • custom modules (e.g. "./untrusted-module.js") that have a function as default export
  • node_modules
  • node-internal modules (e.g. crypto)

Important: the custom (untrusted) modules need to be able to load further modules (which ones is not known in advance). Also, I don't necessarily need compartment separation between loaded modules; as long as the custom untrusted module cannot reach beyond its compartment.

My approach is as follows:

import { resolve } from 'import-meta-resolve'
import { StaticModuleRecord } from '@endo/static-module-record'
import { fileURLToPath } from 'url'
import * as fs from 'fs'

lockdown()

const compartment = new Compartment({ fs: mockFs, net: mockNet }, {}, {
  resolveHook: moduleSpecifier => moduleSpecifier,
  importHook: async moduleSpecifier => {
    if(moduleSpecifier.startWith('node:') {
      // QUESTION: how to load node-internal modules?
    } else {
      const filePath = fileURLToPath(await resolve(moduleSpecifier, import.meta.url))
      const code = await fs.promises.readFile(filePath, { encoding: 'utf-8' })
      // QUESTION: is this really the most efficient way to load a module?
      return new StaticModuleRecord(code, filePath)
    }
  }
}

const untrustedModule = compartment.import('./untrusted-module.js')
const untrustedFunction = untrustedModule.namespace.default
untrustedFunction()

Could you please advise what the best way would be to load node-internal modules and other modules? I'm also very open to suggestions that would make my approach more solid / easier to work with :-).

Thanks and best regards

sumbricht avatar Dec 23 '22 23:12 sumbricht

Perhaps I can pass this question to @naugtur who worked on this for LavaMoat bundles. The answer is that we currently have to synthesize a compartment module namespace from the Node.js namespace. We could potentially make this more ergonomic by having SES do use dynamic import internally if there were a way to express that in a module descriptor like { specifier: 'node:fs', something }.

kriskowal avatar Jan 08 '24 21:01 kriskowal

You can look at how it's done in https://github.com/endojs/endo-e2e-tests/blob/main/test-imports/tools/core-modules.mjs as an example.

If you use endo/compartment-mapper, it has an importHook option that's gonna fire for exit modules (modules not present in dependency graph) and there you don't need the compartment wrapping part, because it's done on a different layer and you can just return a record with a module namespace.

naugtur avatar Jan 09 '24 21:01 naugtur