Skip to content

Commit

Permalink
Support nested permissions
Browse files Browse the repository at this point in the history
Closes: #73
Refs: #75

PR-URL: #74
  • Loading branch information
tshemsedinov committed Apr 19, 2022
1 parent eef3cd2 commit 7beeaf2
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 18 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [Unreleased][unreleased]

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

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

Expand Down
9 changes: 9 additions & 0 deletions metavm.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Context, Script, ScriptOptions, BaseOptions } from 'vm';
export const EMPTY_CONTEXT: Context;
export const COMMON_CONTEXT: Context;

export class MetavmError extends Error {}

export function createContext(
context?: Context,
preventEscape?: boolean
Expand Down Expand Up @@ -30,3 +32,10 @@ export function readScript(
filePath: string,
options?: BaseOptions
): Promise<MetaScript>;

export function metarequire(options: {
dirname: string;
relative: string;
context: Context;
access: object;
}): MetaScript;
56 changes: 49 additions & 7 deletions metavm.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const path = require('path');
const RUN_OPTIONS = { timeout: 5000, displayErrors: false };
const CONTEXT_OPTIONS = { microtaskMode: 'afterEvaluate' };
const USE_STRICT = `'use strict';\n`;
const CURDIR = '.' + path.sep;

const EMPTY_CONTEXT = vm.createContext(Object.freeze({}));

Expand All @@ -29,6 +30,8 @@ const COMMON_CONTEXT = vm.createContext(
})
);

class MetavmError extends Error {}

const createContext = (context, preventEscape = false) => {
if (context === undefined) return EMPTY_CONTEXT;
return vm.createContext(context, preventEscape ? CONTEXT_OPTIONS : {});
Expand Down Expand Up @@ -60,19 +63,58 @@ const readScript = async (filePath, options) => {
return script;
};

const metarequire = (context, permitted = {}) => {
const internalRequire = require;

const checkAccess = (access, name) => {
for (const key of Object.keys(access)) {
if (name.startsWith(key)) return Reflect.get(access, key);
}
};

const metarequire = (options) => {
const { dirname = process.cwd(), relative = '.' } = options;
const context = createContext({ ...options.context });
const access = { ...options.access };

const require = (module) => {
if (Reflect.has(permitted, module)) {
return Reflect.get(permitted, module);
let name = module;
let lib = checkAccess(access, name);
if (lib instanceof Object) return lib;
const npm = !name.includes('.');
if (!npm) {
name = path.resolve(dirname, relative, module);
let rel = CURDIR + path.relative(dirname, name);
lib = checkAccess(access, rel);
if (lib instanceof Object) return lib;
const ext = name.toLocaleLowerCase().endsWith('.js') ? '' : '.js';
const js = name + ext;
name = name.startsWith('.') ? path.resolve(dirname, js) : js;
rel = CURDIR + path.relative(dirname, js);
lib = checkAccess(access, rel);
if (lib instanceof Object) return lib;
}
if (!lib) throw new MetavmError(`Access denied '${module}'`);
try {
const src = fs.readFileSync(module, 'utf8');
const absolute = internalRequire.resolve(name);
if (npm && absolute === name) return internalRequire(name);
let relative = path.dirname(absolute);
if (npm) {
const dirname = relative;
const access = { ...options.access, [CURDIR]: true };
relative = '.';
context.require = metarequire({ dirname, relative, context, access });
} else {
context.require = metarequire({ dirname, relative, context, access });
}
const src = fs.readFileSync(absolute, 'utf8');
const script = createScript(module, src, { context });
return script;
} catch {
throw new Error(`Cannot find module: '${module}'`);
return script.exports;
} catch (err) {
if (err instanceof MetavmError) throw err;
throw new MetavmError(`Cannot find module '${module}'`);
}
};

return require;
};

Expand Down
2 changes: 1 addition & 1 deletion test/examples/nestedmodule1.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
({
name: 'nestedmodule1',
value: 1,
nested: require('./test/examples/nestedmodule2.js'),
nested: require('./nestedmodule2.js'),
});
/* eslint-enable */
69 changes: 59 additions & 10 deletions test/unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,11 @@ metatests.test('Call undefined as a function', async (test) => {
metatests.test('Metarequire node internal module', async (test) => {
const sandbox = {};
sandbox.global = sandbox;
sandbox.require = metavm.metarequire(sandbox, { fs });
sandbox.require = metavm.metarequire({
dirname: __dirname,
sandbox,
access: { fs },
});
const context = metavm.createContext(sandbox);
const src = `({ fs: require('fs') })`;
const ms = metavm.createScript('Example', src, { context });
Expand All @@ -201,41 +205,86 @@ metatests.test('Metarequire node internal module', async (test) => {
metatests.test('Metarequire internal not permitted', async (test) => {
const sandbox = {};
sandbox.global = sandbox;
sandbox.require = metavm.metarequire(sandbox);
sandbox.require = metavm.metarequire({ dirname: __dirname, sandbox });
const context = metavm.createContext(sandbox);
const src = `({ fs: require('fs') })`;
try {
const ms = metavm.createScript('Example', src, { context });
test.strictSame(ms, undefined);
} catch (err) {
test.strictSame(err.message, `Cannot find module: 'fs'`);
test.strictSame(err.message, `Access denied 'fs'`);
}
test.end();
});

metatests.test('Metarequire non-existent module', async (test) => {
metatests.test('Metarequire not permitted module', async (test) => {
const sandbox = {};
sandbox.global = sandbox;
sandbox.require = metavm.metarequire(sandbox);
sandbox.require = metavm.metarequire({ dirname: __dirname, sandbox });
const context = metavm.createContext(sandbox);
const src = `({ notExist: require('nothing') })`;
try {
const ms = metavm.createScript('Example', src, { context });
test.strictSame(ms, undefined);
} catch (err) {
test.strictSame(err.message, `Cannot find module: 'nothing'`);
test.strictSame(err.message, `Access denied 'nothing'`);
}
test.end();
});

metatests.test('Metarequire non-existent module', async (test) => {
const sandbox = {};
sandbox.global = sandbox;
sandbox.require = metavm.metarequire({
dirname: __dirname,
sandbox,
access: { metalog: true },
});
const context = metavm.createContext(sandbox);
const src = `({ notExist: require('metalog') })`;
try {
const ms = metavm.createScript('Example', src, { context });
test.strictSame(ms, undefined);
} catch (err) {
test.strictSame(err.message, `Cannot find module 'metalog'`);
}
test.end();
});

metatests.test('Metarequire nestsed modules', async (test) => {
const sandbox = {};
sandbox.global = sandbox;
sandbox.require = metavm.metarequire(sandbox);
const access = {
'./examples/nestedmodule1.js': true,
'./examples/nestedmodule2.js': true,
};
sandbox.require = metavm.metarequire({ dirname: __dirname, sandbox, access });
const context = metavm.createContext(sandbox);
const src = `({ loaded: require('./test/examples/nestedmodule1.js') })`;
const src = `({ loaded: require('./examples/nestedmodule1.js') })`;
const ms = metavm.createScript('Example', src, { context });
test.strictSame(ms.exports.loaded.exports.value, 1);
test.strictSame(ms.exports.loaded.exports.nested.exports.value, 2);
test.strictSame(ms.exports.loaded.value, 1);
test.strictSame(ms.exports.loaded.nested.value, 2);
test.end();
});

metatests.test('Metarequire nestsed not permitted', async (test) => {
const sandbox = {};
sandbox.global = sandbox;
sandbox.require = metavm.metarequire({
dirname: __dirname,
sandbox,
access: {
'./examples/nestedmodule1.js': true,
},
});
const context = metavm.createContext(sandbox);
const src = `({ loaded: require('./examples/nestedmodule1.js') })`;
try {
const ms = metavm.createScript('Example', src, { context });
test.fail('Should not be loaded', ms);
} catch (err) {
const module2 = './nestedmodule2.js';
test.strictSame(err.message, `Access denied '${module2}'`);
}
test.end();
});

0 comments on commit 7beeaf2

Please sign in to comment.