Skip to content

Commit 4cc54cb

Browse files
committed
Support nested permissions
Closes: #73 Refs: #75
1 parent eef3cd2 commit 4cc54cb

File tree

5 files changed

+118
-18
lines changed

5 files changed

+118
-18
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## [Unreleased][unreleased]
44

55
- Implement metarequire and require nesting
6+
- Support permissions for node.js modules (including internal but not for npm)
67

78
## [1.1.0][] - 2022-03-15
89

metavm.d.ts

+9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { Context, Script, ScriptOptions, BaseOptions } from 'vm';
33
export const EMPTY_CONTEXT: Context;
44
export const COMMON_CONTEXT: Context;
55

6+
export class MetavmError extends Error {}
7+
68
export function createContext(
79
context?: Context,
810
preventEscape?: boolean
@@ -30,3 +32,10 @@ export function readScript(
3032
filePath: string,
3133
options?: BaseOptions
3234
): Promise<MetaScript>;
35+
36+
export function metarequire(options: {
37+
dirname: string;
38+
relative: string;
39+
context: Context;
40+
access: object;
41+
}): MetaScript;

metavm.js

+48-7
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ const COMMON_CONTEXT = vm.createContext(
2929
})
3030
);
3131

32+
class MetavmError extends Error {}
33+
3234
const createContext = (context, preventEscape = false) => {
3335
if (context === undefined) return EMPTY_CONTEXT;
3436
return vm.createContext(context, preventEscape ? CONTEXT_OPTIONS : {});
@@ -60,19 +62,58 @@ const readScript = async (filePath, options) => {
6062
return script;
6163
};
6264

63-
const metarequire = (context, permitted = {}) => {
65+
const internalRequire = require;
66+
67+
const checkAccess = (access, name) => {
68+
for (const key of Object.keys(access)) {
69+
if (name.startsWith(key)) return Reflect.get(access, key);
70+
}
71+
};
72+
73+
const metarequire = (options) => {
74+
const { dirname = process.cwd(), relative = '.' } = options;
75+
const context = createContext({ ...options.context });
76+
const access = { ...options.access };
77+
6478
const require = (module) => {
65-
if (Reflect.has(permitted, module)) {
66-
return Reflect.get(permitted, module);
79+
let name = module;
80+
let lib = checkAccess(access, name);
81+
if (lib instanceof Object) return lib;
82+
const npm = !name.includes('.');
83+
if (!npm) {
84+
name = path.resolve(dirname, relative, module);
85+
let rel = './' + path.relative(dirname, name);
86+
lib = checkAccess(access, rel);
87+
if (lib instanceof Object) return lib;
88+
const ext = name.toLocaleLowerCase().endsWith('.js') ? '' : '.js';
89+
const js = name + ext;
90+
name = name.startsWith('.') ? path.resolve(dirname, js) : js;
91+
rel = './' + path.relative(dirname, js);
92+
lib = checkAccess(access, rel);
93+
if (lib instanceof Object) return lib;
6794
}
95+
if (!lib) throw new MetavmError(`Access denied '${module}'`);
6896
try {
69-
const src = fs.readFileSync(module, 'utf8');
97+
const absolute = internalRequire.resolve(name);
98+
if (npm && absolute === name) return internalRequire(name);
99+
let relative = path.dirname(absolute);
100+
if (npm) {
101+
const dirname = relative;
102+
const access = { ...options.access, './': true };
103+
relative = '.';
104+
context.require = metarequire({ dirname, relative, context, access });
105+
} else {
106+
context.require = metarequire({ dirname, relative, context, access });
107+
}
108+
const src = fs.readFileSync(absolute, 'utf8');
70109
const script = createScript(module, src, { context });
71-
return script;
72-
} catch {
73-
throw new Error(`Cannot find module: '${module}'`);
110+
return script.exports;
111+
} catch (err) {
112+
if (err instanceof MetavmError) throw err;
113+
throw new MetavmError(`Cannot find module '${module}'`);
74114
}
75115
};
116+
76117
return require;
77118
};
78119

test/examples/nestedmodule1.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
({
33
name: 'nestedmodule1',
44
value: 1,
5-
nested: require('./test/examples/nestedmodule2.js'),
5+
nested: require('./nestedmodule2.js'),
66
});
77
/* eslint-enable */

test/unit.js

+59-10
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,11 @@ metatests.test('Call undefined as a function', async (test) => {
189189
metatests.test('Metarequire node internal module', async (test) => {
190190
const sandbox = {};
191191
sandbox.global = sandbox;
192-
sandbox.require = metavm.metarequire(sandbox, { fs });
192+
sandbox.require = metavm.metarequire({
193+
dirname: __dirname,
194+
sandbox,
195+
access: { fs },
196+
});
193197
const context = metavm.createContext(sandbox);
194198
const src = `({ fs: require('fs') })`;
195199
const ms = metavm.createScript('Example', src, { context });
@@ -201,41 +205,86 @@ metatests.test('Metarequire node internal module', async (test) => {
201205
metatests.test('Metarequire internal not permitted', async (test) => {
202206
const sandbox = {};
203207
sandbox.global = sandbox;
204-
sandbox.require = metavm.metarequire(sandbox);
208+
sandbox.require = metavm.metarequire({ dirname: __dirname, sandbox });
205209
const context = metavm.createContext(sandbox);
206210
const src = `({ fs: require('fs') })`;
207211
try {
208212
const ms = metavm.createScript('Example', src, { context });
209213
test.strictSame(ms, undefined);
210214
} catch (err) {
211-
test.strictSame(err.message, `Cannot find module: 'fs'`);
215+
test.strictSame(err.message, `Access denied 'fs'`);
212216
}
213217
test.end();
214218
});
215219

216-
metatests.test('Metarequire non-existent module', async (test) => {
220+
metatests.test('Metarequire not permitted module', async (test) => {
217221
const sandbox = {};
218222
sandbox.global = sandbox;
219-
sandbox.require = metavm.metarequire(sandbox);
223+
sandbox.require = metavm.metarequire({ dirname: __dirname, sandbox });
220224
const context = metavm.createContext(sandbox);
221225
const src = `({ notExist: require('nothing') })`;
222226
try {
223227
const ms = metavm.createScript('Example', src, { context });
224228
test.strictSame(ms, undefined);
225229
} catch (err) {
226-
test.strictSame(err.message, `Cannot find module: 'nothing'`);
230+
test.strictSame(err.message, `Access denied 'nothing'`);
231+
}
232+
test.end();
233+
});
234+
235+
metatests.test('Metarequire non-existent module', async (test) => {
236+
const sandbox = {};
237+
sandbox.global = sandbox;
238+
sandbox.require = metavm.metarequire({
239+
dirname: __dirname,
240+
sandbox,
241+
access: { metalog: true },
242+
});
243+
const context = metavm.createContext(sandbox);
244+
const src = `({ notExist: require('metalog') })`;
245+
try {
246+
const ms = metavm.createScript('Example', src, { context });
247+
test.strictSame(ms, undefined);
248+
} catch (err) {
249+
test.strictSame(err.message, `Cannot find module 'metalog'`);
227250
}
228251
test.end();
229252
});
230253

231254
metatests.test('Metarequire nestsed modules', async (test) => {
232255
const sandbox = {};
233256
sandbox.global = sandbox;
234-
sandbox.require = metavm.metarequire(sandbox);
257+
const access = {
258+
'./examples/nestedmodule1.js': true,
259+
'./examples/nestedmodule2.js': true,
260+
};
261+
sandbox.require = metavm.metarequire({ dirname: __dirname, sandbox, access });
235262
const context = metavm.createContext(sandbox);
236-
const src = `({ loaded: require('./test/examples/nestedmodule1.js') })`;
263+
const src = `({ loaded: require('./examples/nestedmodule1.js') })`;
237264
const ms = metavm.createScript('Example', src, { context });
238-
test.strictSame(ms.exports.loaded.exports.value, 1);
239-
test.strictSame(ms.exports.loaded.exports.nested.exports.value, 2);
265+
test.strictSame(ms.exports.loaded.value, 1);
266+
test.strictSame(ms.exports.loaded.nested.value, 2);
267+
test.end();
268+
});
269+
270+
metatests.test('Metarequire nestsed not permitted', async (test) => {
271+
const sandbox = {};
272+
sandbox.global = sandbox;
273+
sandbox.require = metavm.metarequire({
274+
dirname: __dirname,
275+
sandbox,
276+
access: {
277+
'./examples/nestedmodule1.js': true,
278+
},
279+
});
280+
const context = metavm.createContext(sandbox);
281+
const src = `({ loaded: require('./examples/nestedmodule1.js') })`;
282+
try {
283+
const ms = metavm.createScript('Example', src, { context });
284+
test.fail('Should not be loaded', ms);
285+
} catch (err) {
286+
const module2 = './nestedmodule2.js';
287+
test.strictSame(err.message, `Access denied '${module2}'`);
288+
}
240289
test.end();
241290
});

0 commit comments

Comments
 (0)