argencoders-notevil
Version:
Evaluate javascript like the built-in eval() method but safely
606 lines (517 loc) • 17.6 kB
JavaScript
var parse = require('esprima').parse
var hoist = require('hoister')
class SafeEvalError extends Error {
constructor(err) {
super(err ? err.message : 'SafeEvalError')
this.innerError = err
}
}
class SyntaxError extends SafeEvalError { }
class RuntimeError extends SafeEvalError { }
var InfiniteChecker = require('./lib/infinite-checker')
var Primitives = require('./lib/primitives')
module.exports = safeEval
module.exports.eval = safeEval
module.exports.prepareAst = prepareAst
module.exports.FunctionFactory = FunctionFactory
module.exports.Function = FunctionFactory()
module.exports.SafeEvalError = SafeEvalError
module.exports.SyntaxError = SyntaxError
module.exports.RuntimeError = RuntimeError
var maxIterations = 1000000
// 'eval' with a controlled environment
function safeEval(src, parentContext) {
var tree = prepareAst(src)
try {
var context = Object.create(parentContext || {})
return finalValue(evaluateAst(tree, context))
} catch (err) {
throw new RuntimeError(err)
}
}
// create a 'Function' constructor for a controlled environment
function FunctionFactory(parentContext) {
var context = Object.create(parentContext || {})
return function Function() {
// normalize arguments array
var args = Array.prototype.slice.call(arguments)
var src = args.slice(-1)[0]
args = args.slice(0, -1)
if (typeof src === 'string') {
//HACK: esprima doesn't like returns outside functions
src = parse('function a(){' + src + '}').body[0].body
}
var tree = prepareAst(src)
return getFunction(tree, args, context)
}
}
// takes an AST or js source and returns an AST
function prepareAst(src) {
let tree
try {
tree = (typeof src === 'string') ? parse(src, { loc: true }) : src
} catch (err) {
throw new SyntaxError(err)
}
return hoist(tree)
}
// evaluate an AST in the given context
function evaluateAst(tree, context) {
var safeFunction = FunctionFactory(context)
var primitives = Primitives(context)
// block scoped context for catch (ex) and 'let'
var blockContext = context
return walk(tree)
// recursively walk every node in an array
function walkAll(nodes) {
var result = undefined
for (var i = 0; i < nodes.length; i++) {
var childNode = nodes[i]
if (childNode.type === 'EmptyStatement') continue
result = walk(childNode)
if (result instanceof ReturnValue) {
return result
}
}
return result
}
// recursively evalutate the node of an AST
function walk(node, traceNode) {
try {
if (!node) return
switch (node.type) {
case 'Program':
return walkAll(node.body)
case 'BlockStatement':
enterBlock()
var result = walkAll(node.body)
leaveBlock()
return result
case 'FunctionDeclaration':
var params = node.params.map(getName)
var value = getFunction(node.body, params, blockContext, node)
return context[node.id.name] = value
case 'ArrowFunctionExpression':
case 'FunctionExpression':
var params = node.params.map(getName)
// HACK: trace the function name for stack traces
if (!node.id && traceNode && traceNode.key && traceNode.key.type === 'Identifier') {
node.id = traceNode.key
}
return getFunction(node.body, params, blockContext, node)
case 'ReturnStatement':
var value = walk(node.argument)
return new ReturnValue('return', value)
case 'BreakStatement':
return new ReturnValue('break')
case 'ContinueStatement':
return new ReturnValue('continue')
case 'ExpressionStatement':
return walk(node.expression)
case 'AssignmentExpression':
return setValue(blockContext, node.left, node.right, node.operator)
case 'UpdateExpression':
return setValue(blockContext, node.argument, null, node.operator)
case 'VariableDeclaration':
node.declarations.forEach(function (declaration) {
var target = node.kind === 'let' ? blockContext : context
if (declaration.init) {
target[declaration.id.name] = walk(declaration.init)
} else {
target[declaration.id.name] = undefined
}
})
break
case 'SwitchStatement':
var defaultHandler = null
var matched = false
var value = walk(node.discriminant)
var result = undefined
enterBlock()
var i = 0
while (result == null) {
if (i < node.cases.length) {
if (node.cases[i].test) { // check or fall through
matched = matched || (walk(node.cases[i].test) === value)
} else if (defaultHandler == null) {
defaultHandler = i
}
if (matched) {
var r = walkAll(node.cases[i].consequent)
if (r instanceof ReturnValue) { // break out
if (r.type == 'break') break
result = r
}
}
i += 1 // continue
} else if (!matched && defaultHandler != null) {
// go back and do the default handler
i = defaultHandler
matched = true
} else {
// nothing we can do
break
}
}
leaveBlock()
return result
case 'IfStatement':
if (walk(node.test)) {
return walk(node.consequent)
} else if (node.alternate) {
return walk(node.alternate)
}
case 'ForStatement':
var infinite = InfiniteChecker(maxIterations)
var result = undefined
enterBlock() // allow lets on delarations
for (walk(node.init); walk(node.test); walk(node.update)) {
var r = walk(node.body)
// handle early return, continue and break
if (r instanceof ReturnValue) {
if (r.type == 'continue') continue
if (r.type == 'break') break
result = r
break
}
infinite.check()
}
leaveBlock()
return result
case 'ForInStatement':
var infinite = InfiniteChecker(maxIterations)
var result = undefined
var value = walk(node.right)
var property = node.left
var target = context
enterBlock()
if (property.type == 'VariableDeclaration') {
walk(property)
property = property.declarations[0].id
if (property.kind === 'let') {
target = blockContext
}
}
for (var key in value) {
setValue(target, property, { type: 'Literal', value: key })
var r = walk(node.body)
// handle early return, continue and break
if (r instanceof ReturnValue) {
if (r.type == 'continue') continue
if (r.type == 'break') break
result = r
break
}
infinite.check()
}
leaveBlock()
return result
case 'WhileStatement':
var infinite = InfiniteChecker(maxIterations)
while (walk(node.test)) {
walk(node.body)
infinite.check()
}
break
case 'TryStatement':
try {
walk(node.block)
} catch (error) {
enterBlock()
var catchClause = node.handler
if (catchClause) {
blockContext[catchClause.param.name] = error
walk(catchClause.body)
}
leaveBlock()
} finally {
if (node.finalizer) {
walk(node.finalizer)
}
}
break
case 'Literal':
return node.value
case 'TemplateElement':
return node.value.raw;
case 'TemplateLiteral':
return node.quasis.map((x, i) => {
let element = walk(x)
if (x.tail) return element
let exp = walk(node.expressions[i])
return element + exp
}).join('')
case 'UnaryExpression':
if (node.operator === 'delete' && node.argument.type === 'MemberExpression') {
var arg = node.argument
var parent = walk(arg.object)
var prop = arg.computed ? walk(arg.property) : arg.property.name
delete parent[prop]
return true
} else {
var val = walk(node.argument)
switch (node.operator) {
case '+': return +val
case '-': return -val
case '~': return ~val
case '!': return !val
case 'typeof': return typeof val
default: return unsupportedExpression(node)
}
}
case 'ArrayExpression':
var obj = blockContext['Array']()
for (var i = 0; i < node.elements.length; i++) {
obj.push(walk(node.elements[i]))
}
return obj
case 'ObjectExpression':
var obj = blockContext['Object']()
for (var i = 0; i < node.properties.length; i++) {
var prop = node.properties[i]
var value = (prop.value === null) ? prop.value : walk(prop.value, prop)
obj[prop.key.value || prop.key.name] = value
}
return obj
case 'NewExpression':
var args = node.arguments.map(function (arg) {
return walk(arg)
})
var target = walk(node.callee)
return primitives.applyNew(target, args)
case 'BinaryExpression':
var l = walk(node.left)
var r = walk(node.right)
switch (node.operator) {
case '==': return l == r
case '===': return l === r
case '!=': return l != r
case '!==': return l !== r
case '+': return l + r
case '-': return l - r
case '*': return l * r
case '/': return l / r
case '%': return l % r
case '<': return l < r
case '<=': return l <= r
case '>': return l > r
case '>=': return l >= r
case '|': return l | r
case '&': return l & r
case '^': return l ^ r
case 'instanceof': return l instanceof r
default: return unsupportedExpression(node)
}
case 'LogicalExpression':
switch (node.operator) {
case '&&': return walk(node.left) && walk(node.right)
case '||': return walk(node.left) || walk(node.right)
default: return unsupportedExpression(node)
}
case 'ThisExpression':
return blockContext['this']
case 'Identifier':
if (node.name === 'undefined') {
return undefined
} else if (hasProperty(blockContext, node.name, primitives)) {
return checkValue(blockContext[node.name])
} else {
throw new ReferenceError(node.name + ' is not defined')
}
case 'CallExpression':
var args = node.arguments.map(function (arg) {
return walk(arg)
})
var object = null
var target = walk(node.callee)
if (node.callee.type === 'MemberExpression') {
object = walk(node.callee.object)
}
if (typeof target !== 'function') {
let name
if (node.callee.type === 'MemberExpression') {
name = node.callee.computed
? getComputedMemberExpressionName(node.callee)
: getMemberExpressionName(node.callee)
} else {
name = node.callee.name
}
throw new TypeError(name + ' is not a function')
}
return checkValue(target.apply(object, args))
case 'MemberExpression':
var obj = walk(node.object)
if (node.computed) {
var prop = walk(node.property)
} else {
var prop = node.property.name
}
obj = primitives.getPropertyObject(obj, prop)
return checkValue(obj[prop]);
case 'ConditionalExpression':
var val = walk(node.test)
return val ? walk(node.consequent) : walk(node.alternate)
case 'EmptyStatement':
return
default:
return unsupportedExpression(node)
}
} catch (ex) {
ex.trace = ex.trace || []
ex.trace.push(node)
throw ex
}
}
// safely retrieve a value
function checkValue(value) {
if (value === Function) {
value = safeFunction
}
return finalValue(value)
}
// block scope context control
function enterBlock() {
blockContext = Object.create(blockContext)
}
function leaveBlock() {
blockContext = Object.getPrototypeOf(blockContext)
}
// set a value in the specified context if allowed
function setValue(object, left, right, operator) {
var name = null
if (left.type === 'Identifier') {
name = left.name
// handle parent context shadowing
object = objectForKey(object, name, primitives)
} else if (left.type === 'MemberExpression') {
if (left.computed) {
name = walk(left.property)
} else {
name = left.property.name
}
object = walk(left.object)
}
// stop built in properties from being able to be changed
if (canSetProperty(object, name, primitives)) {
switch (operator) {
case undefined: return object[name] = walk(right)
case '=': return object[name] = walk(right)
case '+=': return object[name] += walk(right)
case '-=': return object[name] -= walk(right)
case '++': return object[name]++
case '--': return object[name]--
}
}
}
}
// when an unsupported expression is encountered, throw an error
function unsupportedExpression(node) {
console.error(node)
var err = new SafeEvalError('Unsupported expression: ' + node.type)
err.node = node
throw err
}
// walk a provided object's prototypal hierarchy to retrieve an inherited object
function objectForKey(object, key, primitives) {
var proto = primitives.getPrototypeOf(object)
if (!proto || hasOwnProperty(object, key)) {
return object
} else {
return objectForKey(proto, key, primitives)
}
}
function hasProperty(object, key, primitives) {
var proto = primitives.getPrototypeOf(object)
var hasOwn = hasOwnProperty(object, key)
if (object[key] !== undefined) {
return true
} else if (!proto || hasOwn) {
return hasOwn
} else {
return hasProperty(proto, key, primitives)
}
}
function hasOwnProperty(object, key) {
return Object.prototype.hasOwnProperty.call(object, key)
}
function propertyIsEnumerable(object, key) {
return Object.prototype.propertyIsEnumerable.call(object, key)
}
// determine if we have write access to a property
function canSetProperty(object, property, primitives) {
if (property === '__proto__' || primitives.isPrimitive(object)) {
return false
} else if (object != null) {
if (hasOwnProperty(object, property)) {
if (propertyIsEnumerable(object, property)) {
return true
} else {
return false
}
} else {
return canSetProperty(primitives.getPrototypeOf(object), property, primitives)
}
} else {
return true
}
}
// generate a function with specified context
function getFunction(body, params, parentContext, traceNode) {
return function () {
try {
var context = Object.create(parentContext)
if (this == global) {
context['this'] = null
} else {
context['this'] = this
}
// normalize arguments array
var args = Array.prototype.slice.call(arguments)
context['arguments'] = arguments
args.forEach(function (arg, idx) {
var param = params[idx]
if (param) {
context[param] = arg
}
})
var result = evaluateAst(body, context)
if (result instanceof ReturnValue) {
return result.value
} else if (traceNode.type === 'ArrowFunctionExpression') {
return result
}
} catch (ex) {
ex.trace = ex.trace || []
ex.trace.push(traceNode)
throw ex
}
}
}
function finalValue(value) {
if (value instanceof ReturnValue) {
return value.value
}
return value
}
// get the name of an identifier
function getName(identifier) {
return identifier.name
}
function getMemberExpressionName(node) {
if (node.object.computed) {
return getComputedMemberExpressionName(node.object) + '.' + getName(node.property)
} else {
return `${getName(node.object)}.${getName(node.property)}`
}
}
function getComputedMemberExpressionName(node) {
let property = node.property.type === 'Literal'
? typeof node.property.value === 'number' ? `[${node.property.raw}]` : `.${node.property.value}`
: unsupportedExpression(node.property)
return `${getName(node.object)}${property}`
}
// a ReturnValue struct for differentiating between expression result and return statement
function ReturnValue(type, value) {
this.type = type
this.value = value
}