410 lines
11 KiB
JavaScript
410 lines
11 KiB
JavaScript
|
|
var aesprim = require('./aesprim');
|
||
|
|
var slice = require('./slice');
|
||
|
|
var _evaluate = require('static-eval');
|
||
|
|
var _uniq = require('underscore').uniq;
|
||
|
|
|
||
|
|
// Property names that must never be accessible in expressions.
|
||
|
|
// Mitigates prototype pollution and constructor escape attacks.
|
||
|
|
var UNSAFE_PROPERTY_NAMES = Object.create(null);
|
||
|
|
|
||
|
|
/* jshint -W069: true */
|
||
|
|
UNSAFE_PROPERTY_NAMES['constructor'] = true;
|
||
|
|
UNSAFE_PROPERTY_NAMES['__proto__'] = true;
|
||
|
|
UNSAFE_PROPERTY_NAMES['prototype'] = true;
|
||
|
|
/* jshint -W069: false */
|
||
|
|
|
||
|
|
function isUnsafePropertyName(name) {
|
||
|
|
return typeof name === 'string' && UNSAFE_PROPERTY_NAMES[name] === true;
|
||
|
|
}
|
||
|
|
|
||
|
|
function isSafeAst(ast) {
|
||
|
|
if (!ast || typeof ast !== 'object') return false;
|
||
|
|
|
||
|
|
function walk(node) {
|
||
|
|
if (!node || typeof node !== 'object' || !node.type) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
switch (node.type) {
|
||
|
|
|
||
|
|
// ===== SAFE TERMINALS =====
|
||
|
|
|
||
|
|
case 'Literal':
|
||
|
|
return true;
|
||
|
|
|
||
|
|
case 'Identifier':
|
||
|
|
// Only allow the special scope identifier
|
||
|
|
return node.name === '@';
|
||
|
|
|
||
|
|
|
||
|
|
// ===== PROPERTY ACCESS =====
|
||
|
|
|
||
|
|
case 'MemberExpression': {
|
||
|
|
if (!walk(node.object)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Non-computed: obj.property
|
||
|
|
if (!node.computed && node.property.type === 'Identifier') {
|
||
|
|
if (isUnsafePropertyName(node.property.name)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Computed: obj["property"]
|
||
|
|
if (node.computed) {
|
||
|
|
if (!walk(node.property)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (
|
||
|
|
node.property.type === 'Literal' &&
|
||
|
|
isUnsafePropertyName(String(node.property.value))
|
||
|
|
) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
// ===== EXPRESSIONS =====
|
||
|
|
|
||
|
|
case 'UnaryExpression':
|
||
|
|
return walk(node.argument);
|
||
|
|
|
||
|
|
case 'BinaryExpression':
|
||
|
|
case 'LogicalExpression':
|
||
|
|
return walk(node.left) && walk(node.right);
|
||
|
|
|
||
|
|
case 'ConditionalExpression':
|
||
|
|
return (
|
||
|
|
walk(node.test) &&
|
||
|
|
walk(node.consequent) &&
|
||
|
|
walk(node.alternate)
|
||
|
|
);
|
||
|
|
|
||
|
|
case 'ArrayExpression':
|
||
|
|
for (var i = 0; i < node.elements.length; i++) {
|
||
|
|
if (!walk(node.elements[i])) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
|
||
|
|
case 'ObjectExpression':
|
||
|
|
for (var j = 0; j < node.properties.length; j++) {
|
||
|
|
var prop = node.properties[j];
|
||
|
|
|
||
|
|
// Reject unsafe keys
|
||
|
|
if (
|
||
|
|
prop.key &&
|
||
|
|
(
|
||
|
|
(prop.key.type === 'Identifier' &&
|
||
|
|
isUnsafePropertyName(prop.key.name)) ||
|
||
|
|
(prop.key.type === 'Literal' &&
|
||
|
|
isUnsafePropertyName(String(prop.key.value)))
|
||
|
|
)
|
||
|
|
) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!walk(prop.value)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
|
||
|
|
|
||
|
|
// ===== EXPLICITLY REJECT DANGEROUS TYPES =====
|
||
|
|
// Security: do not rely on default deny; list each code-execution / escape vector.
|
||
|
|
|
||
|
|
case 'CallExpression':
|
||
|
|
case 'NewExpression':
|
||
|
|
case 'FunctionExpression':
|
||
|
|
case 'ArrowFunctionExpression':
|
||
|
|
case 'ThisExpression':
|
||
|
|
case 'AssignmentExpression':
|
||
|
|
case 'UpdateExpression':
|
||
|
|
case 'SequenceExpression':
|
||
|
|
case 'TemplateLiteral':
|
||
|
|
case 'TemplateElement':
|
||
|
|
case 'TaggedTemplateExpression':
|
||
|
|
case 'ReturnStatement':
|
||
|
|
case 'ExpressionStatement':
|
||
|
|
return false;
|
||
|
|
|
||
|
|
|
||
|
|
// ===== DEFAULT DENY =====
|
||
|
|
|
||
|
|
default:
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return walk(ast);
|
||
|
|
}
|
||
|
|
|
||
|
|
var Handlers = function() {
|
||
|
|
return this.initialize.apply(this, arguments);
|
||
|
|
}
|
||
|
|
|
||
|
|
Handlers.prototype.initialize = function() {
|
||
|
|
this.traverse = traverser(true);
|
||
|
|
this.descend = traverser();
|
||
|
|
}
|
||
|
|
|
||
|
|
Handlers.prototype.keys = Object.keys;
|
||
|
|
|
||
|
|
Handlers.prototype.resolve = function(component) {
|
||
|
|
|
||
|
|
var key = [ component.operation, component.scope, component.expression.type ].join('-');
|
||
|
|
var method = this._fns[key];
|
||
|
|
|
||
|
|
if (!method) throw new Error("couldn't resolve key: " + key);
|
||
|
|
return method.bind(this);
|
||
|
|
};
|
||
|
|
|
||
|
|
Handlers.prototype.register = function(key, handler) {
|
||
|
|
|
||
|
|
if (!handler instanceof Function) {
|
||
|
|
throw new Error("handler must be a function");
|
||
|
|
}
|
||
|
|
|
||
|
|
this._fns[key] = handler;
|
||
|
|
};
|
||
|
|
|
||
|
|
Handlers.prototype._fns = {
|
||
|
|
|
||
|
|
'member-child-identifier': function(component, partial) {
|
||
|
|
var key = component.expression.value;
|
||
|
|
var value = partial.value;
|
||
|
|
if (value instanceof Object && key in value) {
|
||
|
|
return [ { value: value[key], path: partial.path.concat(key) } ]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
'member-descendant-identifier':
|
||
|
|
_traverse(function(key, value, ref) { return key == ref }),
|
||
|
|
|
||
|
|
'subscript-child-numeric_literal':
|
||
|
|
_descend(function(key, value, ref) { return key === ref }),
|
||
|
|
|
||
|
|
'member-child-numeric_literal':
|
||
|
|
_descend(function(key, value, ref) { return String(key) === String(ref) }),
|
||
|
|
|
||
|
|
'subscript-descendant-numeric_literal':
|
||
|
|
_traverse(function(key, value, ref) { return key === ref }),
|
||
|
|
|
||
|
|
'member-child-wildcard':
|
||
|
|
_descend(function() { return true }),
|
||
|
|
|
||
|
|
'member-descendant-wildcard':
|
||
|
|
_traverse(function() { return true }),
|
||
|
|
|
||
|
|
'subscript-descendant-wildcard':
|
||
|
|
_traverse(function() { return true }),
|
||
|
|
|
||
|
|
'subscript-child-wildcard':
|
||
|
|
_descend(function() { return true }),
|
||
|
|
|
||
|
|
'subscript-child-slice': function(component, partial) {
|
||
|
|
if (is_array(partial.value)) {
|
||
|
|
var args = component.expression.value.split(':').map(_parse_nullable_int);
|
||
|
|
var values = partial.value.map(function(v, i) { return { value: v, path: partial.path.concat(i) } });
|
||
|
|
return slice.apply(null, [values].concat(args));
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
'subscript-child-union': function(component, partial) {
|
||
|
|
var results = [];
|
||
|
|
component.expression.value.forEach(function(component) {
|
||
|
|
var _component = { operation: 'subscript', scope: 'child', expression: component.expression };
|
||
|
|
var handler = this.resolve(_component);
|
||
|
|
var _results = handler(_component, partial);
|
||
|
|
if (_results) {
|
||
|
|
results = results.concat(_results);
|
||
|
|
}
|
||
|
|
}, this);
|
||
|
|
|
||
|
|
return unique(results);
|
||
|
|
},
|
||
|
|
|
||
|
|
'subscript-descendant-union': function(component, partial, count) {
|
||
|
|
|
||
|
|
var jp = require('..');
|
||
|
|
var self = this;
|
||
|
|
|
||
|
|
var results = [];
|
||
|
|
var nodes = jp.nodes(partial, '$..*').slice(1);
|
||
|
|
|
||
|
|
nodes.forEach(function(node) {
|
||
|
|
if (results.length >= count) return;
|
||
|
|
component.expression.value.forEach(function(component) {
|
||
|
|
var _component = { operation: 'subscript', scope: 'child', expression: component.expression };
|
||
|
|
var handler = self.resolve(_component);
|
||
|
|
var _results = handler(_component, node);
|
||
|
|
results = results.concat(_results);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
return unique(results);
|
||
|
|
},
|
||
|
|
|
||
|
|
'subscript-child-filter_expression': function(component, partial, count) {
|
||
|
|
|
||
|
|
// slice out the expression from ?(expression)
|
||
|
|
var src = component.expression.value.slice(2, -1);
|
||
|
|
var ast = aesprim.parse(src).body[0].expression;
|
||
|
|
|
||
|
|
var passable = function(key, value) {
|
||
|
|
return evaluate(ast, { '@': value });
|
||
|
|
}
|
||
|
|
|
||
|
|
return this.descend(partial, null, passable, count);
|
||
|
|
|
||
|
|
},
|
||
|
|
|
||
|
|
'subscript-descendant-filter_expression': function(component, partial, count) {
|
||
|
|
|
||
|
|
// slice out the expression from ?(expression)
|
||
|
|
var src = component.expression.value.slice(2, -1);
|
||
|
|
var ast = aesprim.parse(src).body[0].expression;
|
||
|
|
|
||
|
|
var passable = function(key, value) {
|
||
|
|
return evaluate(ast, { '@': value });
|
||
|
|
}
|
||
|
|
|
||
|
|
return this.traverse(partial, null, passable, count);
|
||
|
|
},
|
||
|
|
|
||
|
|
'subscript-child-script_expression': function(component, partial) {
|
||
|
|
var exp = component.expression.value.slice(1, -1);
|
||
|
|
return eval_recurse(partial, exp, '$[{{value}}]');
|
||
|
|
},
|
||
|
|
|
||
|
|
'member-child-script_expression': function(component, partial) {
|
||
|
|
var exp = component.expression.value.slice(1, -1);
|
||
|
|
return eval_recurse(partial, exp, '$.{{value}}');
|
||
|
|
},
|
||
|
|
|
||
|
|
'member-descendant-script_expression': function(component, partial) {
|
||
|
|
var exp = component.expression.value.slice(1, -1);
|
||
|
|
return eval_recurse(partial, exp, '$..value');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
Handlers.prototype._fns['subscript-child-string_literal'] =
|
||
|
|
Handlers.prototype._fns['member-child-identifier'];
|
||
|
|
|
||
|
|
Handlers.prototype._fns['member-descendant-numeric_literal'] =
|
||
|
|
Handlers.prototype._fns['subscript-descendant-string_literal'] =
|
||
|
|
Handlers.prototype._fns['member-descendant-identifier'];
|
||
|
|
|
||
|
|
function eval_recurse(partial, src, template) {
|
||
|
|
|
||
|
|
var jp = require('./index');
|
||
|
|
var ast = aesprim.parse(src).body[0].expression;
|
||
|
|
var value = evaluate(ast, { '@': partial.value });
|
||
|
|
var path = template.replace(/\{\{\s*value\s*\}\}/g, value);
|
||
|
|
|
||
|
|
var results = jp.nodes(partial.value, path);
|
||
|
|
results.forEach(function(r) {
|
||
|
|
r.path = partial.path.concat(r.path.slice(1));
|
||
|
|
});
|
||
|
|
|
||
|
|
return results;
|
||
|
|
}
|
||
|
|
|
||
|
|
function is_array(val) {
|
||
|
|
return Array.isArray(val);
|
||
|
|
}
|
||
|
|
|
||
|
|
function is_object(val) {
|
||
|
|
// is this a non-array, non-null object?
|
||
|
|
return val && !(val instanceof Array) && val instanceof Object;
|
||
|
|
}
|
||
|
|
|
||
|
|
function traverser(recurse) {
|
||
|
|
|
||
|
|
return function(partial, ref, passable, count) {
|
||
|
|
|
||
|
|
var value = partial.value;
|
||
|
|
var path = partial.path;
|
||
|
|
|
||
|
|
var results = [];
|
||
|
|
|
||
|
|
var descend = function(value, path) {
|
||
|
|
|
||
|
|
if (is_array(value)) {
|
||
|
|
value.forEach(function(element, index) {
|
||
|
|
if (results.length >= count) { return }
|
||
|
|
if (passable(index, element, ref)) {
|
||
|
|
results.push({ path: path.concat(index), value: element });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
value.forEach(function(element, index) {
|
||
|
|
if (results.length >= count) { return }
|
||
|
|
if (recurse) {
|
||
|
|
descend(element, path.concat(index));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} else if (is_object(value)) {
|
||
|
|
this.keys(value).forEach(function(k) {
|
||
|
|
if (results.length >= count) { return }
|
||
|
|
if (passable(k, value[k], ref)) {
|
||
|
|
results.push({ path: path.concat(k), value: value[k] });
|
||
|
|
}
|
||
|
|
})
|
||
|
|
this.keys(value).forEach(function(k) {
|
||
|
|
if (results.length >= count) { return }
|
||
|
|
if (recurse) {
|
||
|
|
descend(value[k], path.concat(k));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}.bind(this);
|
||
|
|
descend(value, path);
|
||
|
|
return results;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function _descend(passable) {
|
||
|
|
return function(component, partial, count) {
|
||
|
|
return this.descend(partial, component.expression.value, passable, count);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function _traverse(passable) {
|
||
|
|
return function(component, partial, count) {
|
||
|
|
return this.traverse(partial, component.expression.value, passable, count);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function evaluate(ast, scope) {
|
||
|
|
if (!isSafeAst(ast)) {
|
||
|
|
throw new Error('Unsafe expression: script and filter expressions may only access the current node (@) with safe property names');
|
||
|
|
}
|
||
|
|
try { return _evaluate(ast, scope) }
|
||
|
|
catch (e) { }
|
||
|
|
}
|
||
|
|
|
||
|
|
function unique(results) {
|
||
|
|
results = results.filter(function(d) { return d })
|
||
|
|
return _uniq(
|
||
|
|
results,
|
||
|
|
function(r) { return r.path.map(function(c) { return String(c).replace('-', '--') }).join('-') }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function _parse_nullable_int(val) {
|
||
|
|
var sval = String(val);
|
||
|
|
return sval.match(/^-?[0-9]+$/) ? parseInt(sval) : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = Handlers;
|