Skip to content

Commit 9f2f7c5

Browse files
committed
Add more configurability for containment, and provide allow list for built-in modules
1 parent 3608853 commit 9f2f7c5

File tree

3 files changed

+75
-48
lines changed

3 files changed

+75
-48
lines changed

security/jsLoader.ts

Lines changed: 72 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Resource } from '../resources/Resource.ts';
22
import { tables, databases } from '../resources/databases.ts';
33
import { readFile } from 'node:fs/promises';
44
import { readFileSync } from 'node:fs';
5-
import { extname, dirname, isAbsolute } from 'node:path';
5+
import { dirname, isAbsolute } from 'node:path';
66
import { pathToFileURL, fileURLToPath } from 'node:url';
77
import { SourceTextModule, SyntheticModule, createContext, runInContext } from 'node:vm';
88
import { Scope } from '../components/Scope.ts';
@@ -12,7 +12,9 @@ import * as env from '../utility/environment/environmentManager';
1212
import { CONFIG_PARAMS } from '../utility/hdbTerms.ts';
1313
import 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);
2224
export 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

271264
declare 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+
}

static/defaultConfig.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ authentication:
2323
analytics:
2424
aggregatePeriod: 60
2525
replicate: false
26-
components:
27-
secureJS: true
26+
applications:
27+
containment: 'compartment'
2828
componentsRoot: null
2929
localStudio:
3030
enabled: true

utility/hdbTerms.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ export const CONFIG_PARAMS = {
430430
AUTHENTICATION_REFRESHTOKENTIMEOUT: 'authentication_refreshTokenTimeout',
431431
AUTHENTICATION_HASHFUNCTION: 'authentication_hashFunction',
432432
CUSTOMFUNCTIONS_NETWORK_HTTPS: 'customFunctions_network_https',
433-
COMPONENTS_SECUREJS: 'components_secureJS',
433+
APPLICATIONS_CONTAINMENT: 'applications_containment',
434434
THREADS: 'threads',
435435
THREADS_COUNT: 'threads_count',
436436
THREADS_DEBUG: 'threads_debug',

0 commit comments

Comments
 (0)