diff --git a/.travis.yml b/.travis.yml index 1f54fc8..25919c9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,4 +4,5 @@ node_js: - "6" - "8" - "10" - - "12" \ No newline at end of file + - "12" + - "14" \ No newline at end of file diff --git a/lib/contextify.js b/lib/contextify.js index 5695e0e..2cd5be3 100644 --- a/lib/contextify.js +++ b/lib/contextify.js @@ -200,6 +200,19 @@ Decontextify.instance = (instance, klass, deepTraps, flags, toStringTag) => { if (key === '__lookupSetter__') return host.Object.prototype.__lookupSetter__; if (key === host.Symbol.toStringTag && toStringTag) return toStringTag; + if (key === host.inspect.custom) { + return (depth, options) => { + try { + options = host.Object.assign(host.Object.create(null), options); + options.customInspect = false; + return host.inspect(instance, options); + } catch (e) { + if (e instanceof host.Error) throw e; + throw Decontextify.value(e); + } + }; + } + try { return Decontextify.value(instance[key], null, deepTraps, flags); } catch (e) { @@ -625,7 +638,7 @@ Contextify.function = (fnc, traps, deepTraps, flags, mock) => { }; base.construct = (target, args, newTarget) => { // Fixes buffer unsafe allocation for node v6/7 - if (host.version < 8 && fnc === host.Buffer && 'number' === typeof args[0]) { + if (host.NODE_MAJOR < 8 && fnc === host.Buffer && 'number' === typeof args[0]) { args[0] = new Array(args[0]).fill(0); } @@ -979,6 +992,7 @@ BufferOverride.inspect = function inspect(recurseTimes, ctx) { const LocalBuffer = global.Buffer = Contextify.readonly(host.Buffer, BufferMock); Contextify.connect(host.Buffer.prototype.inspect, BufferOverride.inspect); +Proxy[host.inspect.custom] = () => '[Function: Proxy]'; const exportsMap = host.Object.create(null); exportsMap.Contextify = Contextify; diff --git a/lib/main.js b/lib/main.js index 746e499..c9d52b8 100644 --- a/lib/main.js +++ b/lib/main.js @@ -23,6 +23,7 @@ const fs = require('fs'); const vm = require('vm'); const pa = require('path'); +const {inspect} = require('util'); const {EventEmitter} = require('events'); const {INSPECT_MAX_BYTES} = require('buffer'); const helpers = require('./helpers.js'); @@ -1246,13 +1247,14 @@ class VMError extends Error { } } +const [major, minor] = process.versions.node.split('.'); + /** * Host objects * * @private */ const HOST = { - version: parseInt(process.versions.node.split('.')[0]), require, process, console, @@ -1286,7 +1288,10 @@ const HOST = { Set, WeakSet, Promise, + inspect, Symbol, + NODE_MAJOR: +major, + NODE_MINOR: +minor, INSPECT_MAX_BYTES, VM, NodeVM, diff --git a/test/vm.js b/test/vm.js index 5c5d802..4eb1a60 100644 --- a/test/vm.js +++ b/test/vm.js @@ -280,6 +280,66 @@ describe('contextify', () => { }); }); +describe('inspect', () => { + let vm; + + before(() => { + const sandbox = { inspect }; + vm = new VM({ sandbox }); + }); + + it('boxed primitives', () => { + assert.equal(inspect(vm.run('new Number(1)')), inspect(new Number(1))); + assert.equal(vm.run('inspect(new Number(1))'), inspect(new Number(1))); + assert.equal(inspect(vm.run('new String(1)')), inspect(new String(1))); + assert.equal(vm.run('inspect(new String(1))'), inspect(new String(1))); + assert.equal(inspect(vm.run('new Boolean(1)')), inspect(new Boolean(1))); + assert.equal(vm.run('inspect(new Boolean(1))'), inspect(new Boolean(1))); + }); + + it('other built-in objects', () => { + assert.equal(inspect(vm.run('Object')), inspect(Object)); + assert.equal(vm.run('inspect(Object)'), inspect(Object)); + assert.equal(inspect(vm.run('Function')), inspect(Function)); + assert.equal(vm.run('inspect(Function)'), inspect(Function)); + assert.equal(inspect(vm.run('Symbol')), inspect(Symbol)); + assert.equal(vm.run('inspect(Symbol)'), inspect(Symbol)); + assert.equal(inspect(vm.run('Reflect')), inspect(Reflect)); + assert.equal(vm.run('inspect(Reflect)'), inspect(Reflect)); + assert.equal(inspect(vm.run('Proxy')), inspect(Proxy)); + assert.equal(vm.run('inspect(Proxy)'), inspect(Proxy)); + assert.equal(inspect(vm.run('JSON')), inspect(JSON)); + assert.equal(vm.run('inspect(JSON)'), inspect(JSON)); + }); + + it('built-in instances', () => { + assert.equal(inspect(vm.run('new Date')).slice(0, -3), inspect(new Date).slice(0, -3)); + assert.equal(vm.run('inspect(new Date)').slice(0, -3), inspect(new Date).slice(0, -3)); + assert.equal(inspect(vm.run('new RegExp("^", "g")')), inspect(new RegExp('^', 'g'))); + assert.equal(vm.run('inspect(new RegExp("^", "g"))'), inspect(new RegExp('^', 'g'))); + assert.equal(inspect(vm.run('new Array(50)')), inspect(new Array(50))); + assert.equal(vm.run('inspect(new Array(50))'), inspect(new Array(50))); + assert.equal(inspect(vm.run('new Set([1])')), inspect(new Set([1]))); + assert.equal(vm.run('inspect(new Set([1]))'), inspect(new Set([1]))); + assert.equal(inspect(vm.run('new Map([[1, 2]])')), inspect(new Map([[1, 2]]))); + assert.equal(vm.run('inspect(new Map([[1, 2]]))'), inspect(new Map([[1, 2]]))); + assert.equal(inspect(vm.run('Promise.resolve(1)')), inspect(Promise.resolve(1))); + assert.equal(vm.run('inspect(Promise.resolve(1))'), inspect(Promise.resolve(1))); + }); + + if (NODE_VERSION > 7) { + // Node until 7 had no async, see https://node.green/ + it('other objects', () => { + const AsyncFunction = eval('Object.getPrototypeOf(async () => {}).constructor'); + const GeneratorFunction = eval('Object.getPrototypeOf(function* f() {}).constructor'); + assert.equal(inspect(vm.run('Object.getPrototypeOf(async () => {}).constructor')), inspect(AsyncFunction)); + assert.equal(vm.run('inspect(Object.getPrototypeOf(async () => {}).constructor)'), inspect(AsyncFunction)); + assert.equal(inspect(vm.run('Object.getPrototypeOf(function* f() {}).constructor')), inspect(GeneratorFunction)); + assert.equal(vm.run('inspect(Object.getPrototypeOf(function* f() {}).constructor)'), inspect(GeneratorFunction)); + }); + } +}); + describe('VM', () => { let vm; @@ -420,13 +480,13 @@ describe('VM', () => { `), /process is not defined/, '#1'); assert.throws(() => vm2.run(` - try { - boom(); - } - catch (e) { - const foreignFunction = e.constructor.constructor; - const process = foreignFunction("return process")(); - } + try { + boom(); + } + catch (e) { + const foreignFunction = e.constructor.constructor; + const process = foreignFunction("return process")(); + } `), /process is not defined/, '#2'); assert.doesNotThrow(() => vm2.run(` @@ -888,6 +948,46 @@ describe('VM', () => { `), /e is not a function/); }); + it('inspect attack', () => { + // https://github.com/patriksimek/vm2/pull/315#issuecomment-673708529 + let vm2 = new VM(); + let badObject = vm2.run(` + const customInspect = Symbol.for('nodejs.util.inspect.custom'); + Date.prototype[customInspect] = (depth, options) => { + const s = options.stylize; + function do_recursive() { + try { + s(); + } catch(e) { + return e; + } + const r = do_recursive(); + if (r) return r; + throw null; + } + throw do_recursive().constructor.constructor("return process;")(); + } + new Proxy(new Date(), { + has(target, key) { + return false; + } + }); + `); + assert.doesNotThrow(() => inspect(badObject)); + + // https://github.com/patriksimek/vm2/pull/315#issuecomment-673708529 + vm2 = new VM(); + badObject = vm2.run(` + const d = new Date(); + new Proxy(d, { + has(target, key) { + throw (f) => f.constructor("return process;")(); + } + }); + `); + assert.doesNotThrow(() => inspect(badObject)); + }); + after(() => { vm = null; });