Skip to content

Commit

Permalink
Merge pull request #246 from XmiliaH/fix-180
Browse files Browse the repository at this point in the history
Add option to disable async
  • Loading branch information
patriksimek committed Mar 20, 2020
2 parents a63bef7 + 9224194 commit ed7215f
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 1 deletion.
110 changes: 110 additions & 0 deletions lib/fixasync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
'use strict';

// eslint-disable-next-line no-invalid-this, no-shadow
const {GeneratorFunction, AsyncFunction, AsyncGeneratorFunction, global, Contextify, host} = this;
// eslint-disable-next-line no-shadow
const {Function, eval: eval_, Promise, Object, Reflect, RegExp, VMError} = global;
const {setPrototypeOf, getOwnPropertyDescriptor, defineProperty} = Object;
const {apply: rApply, construct: rConstruct} = Reflect;
const {test} = RegExp.prototype;

function rejectAsync() {
throw new VMError('Async not available');
}

const testAsync = setPrototypeOf(/\basync\b/, null);

function checkAsync(source) {
// Filter async functions, await can only be in them.
if (rApply(test, testAsync, [source])) {
throw rejectAsync();
}
return source;
}

const AsyncCheckHandler = {
__proto__: null,
apply(target, thiz, args) {
let i;
for (i=0; i<args.length; i++) {
// We want a exception here if args[i] is a Symbol
// since Function does the same thing
args[i] = checkAsync('' + args[i]);
}
return rApply(target, undefined, args);
},
construct(target, args, newTarget) {
let i;
for (i=0; i<args.length; i++) {
// We want a exception here if args[i] is a Symbol
// since Function does the same thing
args[i] = checkAsync('' + args[i]);
}
return rConstruct(target, args);
}
};

const AsyncEvalHandler = {
__proto__: null,
apply(target, thiz, args) {
if (args.length === 0) return undefined;
const script = args[0];
if (typeof script !== 'string') {
// Eval does the same thing
return script;
}
checkAsync(script);
return eval_(script);
}
};

function override(obj, prop, value) {
const desc = getOwnPropertyDescriptor(obj, prop);
desc.value = value;
defineProperty(obj, prop, desc);
}

const proxiedFunction = new host.Proxy(Function, AsyncCheckHandler);
override(Function.prototype, 'constructor', proxiedFunction);
if (GeneratorFunction) {
Object.setPrototypeOf(GeneratorFunction, proxiedFunction);
override(GeneratorFunction.prototype, 'constructor', new host.Proxy(GeneratorFunction, AsyncCheckHandler));
}
if (AsyncFunction || AsyncGeneratorFunction) {
const AsyncFunctionRejectHandler = {
__proto__: null,
apply: rejectAsync,
construct: rejectAsync
};
if (AsyncFunction) {
Object.setPrototypeOf(AsyncFunction, proxiedFunction);
override(AsyncFunction.prototype, 'constructor', new host.Proxy(AsyncFunction, AsyncFunctionRejectHandler));
}
if (AsyncGeneratorFunction) {
Object.setPrototypeOf(AsyncGeneratorFunction, proxiedFunction);
override(AsyncGeneratorFunction.prototype, 'constructor', new host.Proxy(AsyncGeneratorFunction, AsyncFunctionRejectHandler));
}
}

global.Function = Function.prototype.constructor;
global.eval = new host.Proxy(eval_, AsyncEvalHandler);

if (Promise) {
const AsyncRejectHandler = {
__proto__: null,
apply: rejectAsync
};

Promise.prototype.then = new host.Proxy(Promise.prototype.then, AsyncRejectHandler);
Contextify.connect(host.Promise.prototype.then, Promise.prototype.then);

if (Promise.prototype.finally) {
Promise.prototype.finally = new host.Proxy(Promise.prototype.finally, AsyncRejectHandler);
Contextify.connect(host.Promise.prototype.finally, Promise.prototype.finally);
}
if (Promise.prototype.catch) {
Promise.prototype.catch = new host.Proxy(Promise.prototype.catch, AsyncRejectHandler);
Contextify.connect(host.Promise.prototype.catch, Promise.prototype.catch);
}

}
37 changes: 36 additions & 1 deletion lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ function loadScript(filename) {
const SCRIPT_CACHE = {
cf: loadScript(`${__dirname}/contextify.js`).wrap('(function(require, host) { ', '\n})')._compileVM(),
sb: loadScript(`${__dirname}/sandbox.js`).wrap('(function (vm, host, Contextify, Decontextify, Buffer) { ', '\n})')._compileVM(),
fa: loadScript(`${__dirname}/fixasync.js`).wrap('(function () { ', '\n})'),
getGlobal: new VMScript('this'),
getGeneratorFunction: new VMScript('(function*(){}).constructor'),
getAsyncFunction: new VMScript('(async function(){}).constructor'),
getAsyncGeneratorFunction: new VMScript('(async function*(){}).constructor'),
exp: new VMScript('({exports: {}})')._compileVM(),
runTimeout: new VMScript('fn()', 'timeout_bridge.js')._compileVM()
};
Expand Down Expand Up @@ -232,7 +237,8 @@ class VM extends EventEmitter {
sandbox: options.sandbox,
compiler: lookupCompiler(options.compiler || 'javascript'),
eval: options.eval === false ? false : true,
wasm: options.wasm === false ? false : true
wasm: options.wasm === false ? false : true,
fixAsync: options.fixAsync
};

const host = {
Expand Down Expand Up @@ -277,6 +283,30 @@ class VM extends EventEmitter {
value: SCRIPT_CACHE.cf._runInVM(this._context).call(this._context, require, host)
});

if (this.options.fixAsync) {
SCRIPT_CACHE.getGlobal._compileVM();
SCRIPT_CACHE.fa._compileVM();
const internal = {
__proto__: null,
global: SCRIPT_CACHE.getGlobal._runInVM(this._context),
Contextify: this._internal.Contextify,
host: host
};
try {
SCRIPT_CACHE.getGeneratorFunction._compileVM();
internal.GeneratorFunction = SCRIPT_CACHE.getGeneratorFunction._runInVM(this._context);
} catch (ex) {}
try {
SCRIPT_CACHE.getAsyncFunction._compileVM();
internal.AsyncFunction = SCRIPT_CACHE.getAsyncFunction._runInVM(this._context);
} catch (ex) {}
try {
SCRIPT_CACHE.getAsyncGeneratorFunction._compileVM();
internal.AsyncGeneratorFunction = SCRIPT_CACHE.getAsyncGeneratorFunction._runInVM(this._context);
} catch (ex) {}
SCRIPT_CACHE.fa._runInVM(this._context).call(internal);
}

// prepare global sandbox
if (this.options.sandbox) {
if ('object' !== typeof this.options.sandbox) {
Expand Down Expand Up @@ -331,6 +361,11 @@ class VM extends EventEmitter {

run(code, filename) {
const script = code instanceof VMScript ? code : new VMScript(code, filename, {compiler: this.options.compiler});

if (this.options.fixAsync && /\basync\b/.test(script.code)) {
throw new VMError('Async not available');
}

script._compileVM();

if (!this.options.timeout) {
Expand Down
13 changes: 13 additions & 0 deletions test/vm.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,19 @@ describe('VM', () => {
});
}

it('async', () => {
const vm2 = new VM({fixAsync: true});
assert.throws(() => vm2.run('(async function(){})'), /Async not available/, '#1');
assert.throws(() => vm2.run('new Function("as"+"ync function(){}")'), /Async not available/, '#2');
assert.throws(() => vm2.run('new (function*(){}).constructor("as"+"ync function(){}")'), /Async not available/, '#3');
assert.throws(() => vm2.run('Promise.resolve().then(function(){})'), /Async not available/, '#4');
if (Promise.prototype.finally) assert.throws(() => vm2.run('Promise.resolve().finally(function(){})'), /Async not available/, '#5');
if (Promise.prototype.catch) assert.throws(() => vm2.run('Promise.resolve().catch(function(){})'), /Async not available/, '#6');
assert.throws(() => vm2.run('eval("as"+"ync function(){}")'), /Async not available/, '#7');
assert.strictEqual(vm2.run('Object.getPrototypeOf((function*(){}).constructor)'), vm2.run('Function'), '#8');
assert.throws(() => vm2.run('Function')('async function(){}'), /Async not available/, '#9');
});

it('various attacks #1', () => {
const vm2 = new VM({sandbox: {log: console.log, boom: () => {
throw new Error();
Expand Down

0 comments on commit ed7215f

Please sign in to comment.