@@ -2,7 +2,7 @@ import { Resource } from '../resources/Resource.ts';
22import { tables , databases } from '../resources/databases.ts' ;
33import { readFile } from 'node:fs/promises' ;
44import { readFileSync } from 'node:fs' ;
5- import { extname , dirname , isAbsolute } from 'node:path' ;
5+ import { dirname , isAbsolute } from 'node:path' ;
66import { pathToFileURL , fileURLToPath } from 'node:url' ;
77import { SourceTextModule , SyntheticModule , createContext , runInContext } from 'node:vm' ;
88import { Scope } from '../components/Scope.ts' ;
@@ -12,7 +12,9 @@ import * as env from '../utility/environment/environmentManager';
1212import { CONFIG_PARAMS } from '../utility/hdbTerms.ts' ;
1313import type { CompartmentOptions } from 'ses' ;
1414
15- const SECURE_JS = env . get ( CONFIG_PARAMS . COMPONENTS_SECUREJS ) ;
15+ type ContainmentMode = 'none' | 'vm' | 'compartment' | 'lockdown' ;
16+ const APPLICATIONS_CONTAINMENT : ContainmentMode = env . get ( CONFIG_PARAMS . APPLICATIONS_CONTAINMENT ) ;
17+
1618/**
1719 * This is the main entry point for loading plugin and application modules that may be executed in a
1820 * separate top level scope. The scope indicates if we use a different top level scope or a standard import.
@@ -22,24 +24,16 @@ const SECURE_JS = env.get(CONFIG_PARAMS.COMPONENTS_SECUREJS);
2224export async function scopedImport ( filePath : string , scope ?: Scope ) {
2325 const moduleUrl = pathToFileURL ( filePath ) . toString ( ) ;
2426 try {
25- if ( scope ) {
26- if ( SECURE_JS ) {
27- // note that we use a single compartment that is used by all the secure JS modules and we load it on-demand, only
28- // loading if necessary (since it is actually very heavy)
29- if ( ! scope . compartment )
30- scope . compartment = getCompartment ( ( ) => {
31- return {
32- server : scope . server ?? server ,
33- logger : scope . logger ?? logger ,
34- resources : scope . resources ,
35- config : scope . options . getRoot ( ) ?? { } ,
36- ...getGlobalVars ( ) ,
37- } ;
38- } ) ;
39- const result = await ( await scope . compartment ) . import ( moduleUrl ) ;
40- return result . namespace ;
41- }
42- return await loadModuleWithVM ( moduleUrl , scope ) ;
27+ if ( scope && APPLICATIONS_CONTAINMENT && APPLICATIONS_CONTAINMENT !== 'none' ) {
28+ const globals = getGlobalVars ( scope ) ;
29+ if ( APPLICATIONS_CONTAINMENT === 'vm' ) {
30+ return await loadModuleWithVM ( moduleUrl , scope ) ;
31+ } // else use SES Compartments
32+ // note that we use a single compartment per scope and we load it on-demand, only
33+ // loading if necessary (since it is actually very heavy)
34+ if ( ! scope . compartment ) scope . compartment = getCompartment ( scope , globals ) ;
35+ const result = await ( await scope . compartment ) . import ( moduleUrl ) ;
36+ return result . namespace ;
4337 } else {
4438 // important! we need to await the import, otherwise the error will not be caught
4539 return await import ( moduleUrl ) ;
@@ -66,12 +60,7 @@ async function loadModuleWithVM(moduleUrl: string, scope: Scope) {
6660 const moduleCache = new Map < string , SourceTextModule | SyntheticModule > ( ) ;
6761
6862 // Create a secure context with limited globals
69- const contextObject = {
70- server : scope ?. server ?? server ,
71- logger : scope ?. logger ?? logger ,
72- config : scope ?. options . getRoot ( ) ?? { } ,
73- ...getGlobalVars ( ) ,
74- } ;
63+ const contextObject = getGlobalVars ( scope ) ;
7564 const context = createContext ( contextObject ) ;
7665
7766 /**
@@ -83,6 +72,9 @@ async function loadModuleWithVM(moduleUrl: string, scope: Scope) {
8372 return specifier ;
8473 }
8574 const resolved = createRequire ( referrer ) . resolve ( specifier ) ;
75+ if ( isAbsolute ( resolved ) ) {
76+ return pathToFileURL ( resolved ) . toString ( ) ;
77+ }
8678 return resolved ;
8779 }
8880
@@ -234,6 +226,7 @@ async function loadModuleWithVM(moduleUrl: string, scope: Scope) {
234226 }
235227 }
236228 } else {
229+ checkAllowedHostModule ( url , scope . directory ) ;
237230 // For Node.js built-in modules (node:) and npm packages, use dynamic import
238231 const importedModule = await import ( url ) ;
239232 const exportNames = Object . keys ( importedModule ) ;
@@ -269,25 +262,20 @@ async function loadModuleWithVM(moduleUrl: string, scope: Scope) {
269262}
270263
271264declare class Compartment extends CompartmentClass { }
272- async function getCompartment ( getGlobalVars ) {
265+ async function getCompartment ( scope : Scope , globals ) {
273266 const { StaticModuleRecord } = await import ( '@endo/static-module-record' ) ;
274267 require ( 'ses' ) ;
275- lockdown ( {
276- domainTaming : 'unsafe' ,
277- consoleTaming : 'unsafe' ,
278- errorTaming : 'unsafe' ,
279- errorTrapping : 'none' ,
280- stackFiltering : 'verbose' ,
281- } ) ;
282-
268+ if ( APPLICATIONS_CONTAINMENT === 'lockdown' ) {
269+ lockdown ( {
270+ domainTaming : 'unsafe' ,
271+ consoleTaming : 'unsafe' ,
272+ errorTaming : 'unsafe' ,
273+ errorTrapping : 'none' ,
274+ stackFiltering : 'verbose' ,
275+ } ) ;
276+ }
283277 const compartment : CompartmentOptions = new ( Compartment as typeof CompartmentOptions ) (
284- {
285- console,
286- Math,
287- Date,
288- fetch : secureOnlyFetch ,
289- ...getGlobalVars ( ) ,
290- } ,
278+ globals ,
291279 {
292280 //harperdb: { Resource, tables, databases }
293281 } ,
@@ -318,6 +306,7 @@ async function getCompartment(getGlobalVars) {
318306 const moduleText = await readFile ( new URL ( moduleSpecifier ) , { encoding : 'utf-8' } ) ;
319307 return new StaticModuleRecord ( moduleText , moduleSpecifier ) ;
320308 } else {
309+ checkAllowedHostModule ( moduleSpecifier , scope . directory ) ;
321310 const moduleExports = await import ( moduleSpecifier ) ;
322311 return {
323312 imports : [ ] ,
@@ -345,15 +334,19 @@ function secureOnlyFetch(resource, options) {
345334 // TODO: or maybe we should constrain by doing a DNS lookup and having disallow list of IP addresses that includes
346335 // this server
347336 const url = typeof resource === 'string' || resource . url ;
348- if ( new URL ( url ) . protocol != 'https' ) throw new Error ( ' Only https is allowed in fetch' ) ;
337+ if ( new URL ( url ) . protocol != 'https' ) throw new Error ( ` Only https is allowed in fetch` ) ;
349338 return fetch ( resource , options ) ;
350339}
351340
352341/**
353- * Get the set of global variables that should be available to the h-dapp modules
342+ * Get the set of global variables that should be available to modules that run in scoped compartments/contexts.
354343 */
355- function getGlobalVars ( ) {
344+ function getGlobalVars ( scope : Scope ) {
356345 return {
346+ server : scope . server ?? server ,
347+ logger : scope . logger ?? logger ,
348+ resources : scope . resources ,
349+ config : scope . options . getRoot ( ) ?? { } ,
357350 Resource,
358351 tables,
359352 process,
@@ -378,3 +371,37 @@ function getGlobalVars() {
378371 fetch : secureOnlyFetch ,
379372 } ;
380373}
374+ const ALLOWED_NODE_BUILTIN_MODULES = new Set ( [
375+ 'assert' ,
376+ 'http' ,
377+ 'https' ,
378+ 'path' ,
379+ 'url' ,
380+ 'util' ,
381+ 'stream' ,
382+ 'crypto' ,
383+ 'buffer' ,
384+ 'string_decoder' ,
385+ 'querystring' ,
386+ 'punycode' ,
387+ 'zlib' ,
388+ 'events' ,
389+ 'timers' ,
390+ 'async_hooks' ,
391+ 'console' ,
392+ 'perf_hooks' ,
393+ 'diagnostics_channel' ,
394+ ] ) ;
395+ function checkAllowedHostModule ( moduleUrl : string , containingFolder : string ) : boolean {
396+ if ( moduleUrl . startsWith ( 'file:' ) ) {
397+ const path = moduleUrl . slice ( 7 ) ;
398+ if ( path . startsWith ( containingFolder ) ) {
399+ return true ;
400+ }
401+ throw new Error ( `Can not load module outside of application folder` ) ;
402+ }
403+ let simpleName = moduleUrl . startsWith ( 'node:' ) ? moduleUrl . slice ( 5 ) : moduleUrl ;
404+ simpleName = simpleName . split ( '/' ) [ 0 ] ;
405+ if ( ALLOWED_NODE_BUILTIN_MODULES . has ( simpleName ) ) return true ;
406+ throw new Error ( `Module ${ moduleUrl } is not allowed to be imported` ) ;
407+ }
0 commit comments