endo
endo copied to clipboard
importHook for node-internal modules?
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
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 }
.
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.