| [9af201e] | 1 | var aesprim = require('./aesprim');
|
|---|
| 2 | var slice = require('./slice');
|
|---|
| 3 | var _evaluate = require('static-eval');
|
|---|
| 4 | var _uniq = require('underscore').uniq;
|
|---|
| 5 |
|
|---|
| 6 | // Property names that must never be accessible in expressions.
|
|---|
| 7 | // Mitigates prototype pollution and constructor escape attacks.
|
|---|
| 8 | var UNSAFE_PROPERTY_NAMES = Object.create(null);
|
|---|
| 9 |
|
|---|
| 10 | /* jshint -W069: true */
|
|---|
| 11 | UNSAFE_PROPERTY_NAMES['constructor'] = true;
|
|---|
| 12 | UNSAFE_PROPERTY_NAMES['__proto__'] = true;
|
|---|
| 13 | UNSAFE_PROPERTY_NAMES['prototype'] = true;
|
|---|
| 14 | /* jshint -W069: false */
|
|---|
| 15 |
|
|---|
| 16 | function isUnsafePropertyName(name) {
|
|---|
| 17 | return typeof name === 'string' && UNSAFE_PROPERTY_NAMES[name] === true;
|
|---|
| 18 | }
|
|---|
| 19 |
|
|---|
| 20 | function isSafeAst(ast) {
|
|---|
| 21 | if (!ast || typeof ast !== 'object') return false;
|
|---|
| 22 |
|
|---|
| 23 | function walk(node) {
|
|---|
| 24 | if (!node || typeof node !== 'object' || !node.type) {
|
|---|
| 25 | return false;
|
|---|
| 26 | }
|
|---|
| 27 |
|
|---|
| 28 | switch (node.type) {
|
|---|
| 29 |
|
|---|
| 30 | // ===== SAFE TERMINALS =====
|
|---|
| 31 |
|
|---|
| 32 | case 'Literal':
|
|---|
| 33 | return true;
|
|---|
| 34 |
|
|---|
| 35 | case 'Identifier':
|
|---|
| 36 | // Only allow the special scope identifier
|
|---|
| 37 | return node.name === '@';
|
|---|
| 38 |
|
|---|
| 39 |
|
|---|
| 40 | // ===== PROPERTY ACCESS =====
|
|---|
| 41 |
|
|---|
| 42 | case 'MemberExpression': {
|
|---|
| 43 | if (!walk(node.object)) {
|
|---|
| 44 | return false;
|
|---|
| 45 | }
|
|---|
| 46 |
|
|---|
| 47 | // Non-computed: obj.property
|
|---|
| 48 | if (!node.computed && node.property.type === 'Identifier') {
|
|---|
| 49 | if (isUnsafePropertyName(node.property.name)) {
|
|---|
| 50 | return false;
|
|---|
| 51 | }
|
|---|
| 52 | return true;
|
|---|
| 53 | }
|
|---|
| 54 |
|
|---|
| 55 | // Computed: obj["property"]
|
|---|
| 56 | if (node.computed) {
|
|---|
| 57 | if (!walk(node.property)) {
|
|---|
| 58 | return false;
|
|---|
| 59 | }
|
|---|
| 60 |
|
|---|
| 61 | if (
|
|---|
| 62 | node.property.type === 'Literal' &&
|
|---|
| 63 | isUnsafePropertyName(String(node.property.value))
|
|---|
| 64 | ) {
|
|---|
| 65 | return false;
|
|---|
| 66 | }
|
|---|
| 67 |
|
|---|
| 68 | return true;
|
|---|
| 69 | }
|
|---|
| 70 |
|
|---|
| 71 | return false;
|
|---|
| 72 | }
|
|---|
| 73 |
|
|---|
| 74 |
|
|---|
| 75 | // ===== EXPRESSIONS =====
|
|---|
| 76 |
|
|---|
| 77 | case 'UnaryExpression':
|
|---|
| 78 | return walk(node.argument);
|
|---|
| 79 |
|
|---|
| 80 | case 'BinaryExpression':
|
|---|
| 81 | case 'LogicalExpression':
|
|---|
| 82 | return walk(node.left) && walk(node.right);
|
|---|
| 83 |
|
|---|
| 84 | case 'ConditionalExpression':
|
|---|
| 85 | return (
|
|---|
| 86 | walk(node.test) &&
|
|---|
| 87 | walk(node.consequent) &&
|
|---|
| 88 | walk(node.alternate)
|
|---|
| 89 | );
|
|---|
| 90 |
|
|---|
| 91 | case 'ArrayExpression':
|
|---|
| 92 | for (var i = 0; i < node.elements.length; i++) {
|
|---|
| 93 | if (!walk(node.elements[i])) {
|
|---|
| 94 | return false;
|
|---|
| 95 | }
|
|---|
| 96 | }
|
|---|
| 97 | return true;
|
|---|
| 98 |
|
|---|
| 99 | case 'ObjectExpression':
|
|---|
| 100 | for (var j = 0; j < node.properties.length; j++) {
|
|---|
| 101 | var prop = node.properties[j];
|
|---|
| 102 |
|
|---|
| 103 | // Reject unsafe keys
|
|---|
| 104 | if (
|
|---|
| 105 | prop.key &&
|
|---|
| 106 | (
|
|---|
| 107 | (prop.key.type === 'Identifier' &&
|
|---|
| 108 | isUnsafePropertyName(prop.key.name)) ||
|
|---|
| 109 | (prop.key.type === 'Literal' &&
|
|---|
| 110 | isUnsafePropertyName(String(prop.key.value)))
|
|---|
| 111 | )
|
|---|
| 112 | ) {
|
|---|
| 113 | return false;
|
|---|
| 114 | }
|
|---|
| 115 |
|
|---|
| 116 | if (!walk(prop.value)) {
|
|---|
| 117 | return false;
|
|---|
| 118 | }
|
|---|
| 119 | }
|
|---|
| 120 | return true;
|
|---|
| 121 |
|
|---|
| 122 |
|
|---|
| 123 | // ===== EXPLICITLY REJECT DANGEROUS TYPES =====
|
|---|
| 124 | // Security: do not rely on default deny; list each code-execution / escape vector.
|
|---|
| 125 |
|
|---|
| 126 | case 'CallExpression':
|
|---|
| 127 | case 'NewExpression':
|
|---|
| 128 | case 'FunctionExpression':
|
|---|
| 129 | case 'ArrowFunctionExpression':
|
|---|
| 130 | case 'ThisExpression':
|
|---|
| 131 | case 'AssignmentExpression':
|
|---|
| 132 | case 'UpdateExpression':
|
|---|
| 133 | case 'SequenceExpression':
|
|---|
| 134 | case 'TemplateLiteral':
|
|---|
| 135 | case 'TemplateElement':
|
|---|
| 136 | case 'TaggedTemplateExpression':
|
|---|
| 137 | case 'ReturnStatement':
|
|---|
| 138 | case 'ExpressionStatement':
|
|---|
| 139 | return false;
|
|---|
| 140 |
|
|---|
| 141 |
|
|---|
| 142 | // ===== DEFAULT DENY =====
|
|---|
| 143 |
|
|---|
| 144 | default:
|
|---|
| 145 | return false;
|
|---|
| 146 | }
|
|---|
| 147 | }
|
|---|
| 148 |
|
|---|
| 149 | return walk(ast);
|
|---|
| 150 | }
|
|---|
| 151 |
|
|---|
| 152 | var Handlers = function() {
|
|---|
| 153 | return this.initialize.apply(this, arguments);
|
|---|
| 154 | }
|
|---|
| 155 |
|
|---|
| 156 | Handlers.prototype.initialize = function() {
|
|---|
| 157 | this.traverse = traverser(true);
|
|---|
| 158 | this.descend = traverser();
|
|---|
| 159 | }
|
|---|
| 160 |
|
|---|
| 161 | Handlers.prototype.keys = Object.keys;
|
|---|
| 162 |
|
|---|
| 163 | Handlers.prototype.resolve = function(component) {
|
|---|
| 164 |
|
|---|
| 165 | var key = [ component.operation, component.scope, component.expression.type ].join('-');
|
|---|
| 166 | var method = this._fns[key];
|
|---|
| 167 |
|
|---|
| 168 | if (!method) throw new Error("couldn't resolve key: " + key);
|
|---|
| 169 | return method.bind(this);
|
|---|
| 170 | };
|
|---|
| 171 |
|
|---|
| 172 | Handlers.prototype.register = function(key, handler) {
|
|---|
| 173 |
|
|---|
| 174 | if (!handler instanceof Function) {
|
|---|
| 175 | throw new Error("handler must be a function");
|
|---|
| 176 | }
|
|---|
| 177 |
|
|---|
| 178 | this._fns[key] = handler;
|
|---|
| 179 | };
|
|---|
| 180 |
|
|---|
| 181 | Handlers.prototype._fns = {
|
|---|
| 182 |
|
|---|
| 183 | 'member-child-identifier': function(component, partial) {
|
|---|
| 184 | var key = component.expression.value;
|
|---|
| 185 | var value = partial.value;
|
|---|
| 186 | if (value instanceof Object && key in value) {
|
|---|
| 187 | return [ { value: value[key], path: partial.path.concat(key) } ]
|
|---|
| 188 | }
|
|---|
| 189 | },
|
|---|
| 190 |
|
|---|
| 191 | 'member-descendant-identifier':
|
|---|
| 192 | _traverse(function(key, value, ref) { return key == ref }),
|
|---|
| 193 |
|
|---|
| 194 | 'subscript-child-numeric_literal':
|
|---|
| 195 | _descend(function(key, value, ref) { return key === ref }),
|
|---|
| 196 |
|
|---|
| 197 | 'member-child-numeric_literal':
|
|---|
| 198 | _descend(function(key, value, ref) { return String(key) === String(ref) }),
|
|---|
| 199 |
|
|---|
| 200 | 'subscript-descendant-numeric_literal':
|
|---|
| 201 | _traverse(function(key, value, ref) { return key === ref }),
|
|---|
| 202 |
|
|---|
| 203 | 'member-child-wildcard':
|
|---|
| 204 | _descend(function() { return true }),
|
|---|
| 205 |
|
|---|
| 206 | 'member-descendant-wildcard':
|
|---|
| 207 | _traverse(function() { return true }),
|
|---|
| 208 |
|
|---|
| 209 | 'subscript-descendant-wildcard':
|
|---|
| 210 | _traverse(function() { return true }),
|
|---|
| 211 |
|
|---|
| 212 | 'subscript-child-wildcard':
|
|---|
| 213 | _descend(function() { return true }),
|
|---|
| 214 |
|
|---|
| 215 | 'subscript-child-slice': function(component, partial) {
|
|---|
| 216 | if (is_array(partial.value)) {
|
|---|
| 217 | var args = component.expression.value.split(':').map(_parse_nullable_int);
|
|---|
| 218 | var values = partial.value.map(function(v, i) { return { value: v, path: partial.path.concat(i) } });
|
|---|
| 219 | return slice.apply(null, [values].concat(args));
|
|---|
| 220 | }
|
|---|
| 221 | },
|
|---|
| 222 |
|
|---|
| 223 | 'subscript-child-union': function(component, partial) {
|
|---|
| 224 | var results = [];
|
|---|
| 225 | component.expression.value.forEach(function(component) {
|
|---|
| 226 | var _component = { operation: 'subscript', scope: 'child', expression: component.expression };
|
|---|
| 227 | var handler = this.resolve(_component);
|
|---|
| 228 | var _results = handler(_component, partial);
|
|---|
| 229 | if (_results) {
|
|---|
| 230 | results = results.concat(_results);
|
|---|
| 231 | }
|
|---|
| 232 | }, this);
|
|---|
| 233 |
|
|---|
| 234 | return unique(results);
|
|---|
| 235 | },
|
|---|
| 236 |
|
|---|
| 237 | 'subscript-descendant-union': function(component, partial, count) {
|
|---|
| 238 |
|
|---|
| 239 | var jp = require('..');
|
|---|
| 240 | var self = this;
|
|---|
| 241 |
|
|---|
| 242 | var results = [];
|
|---|
| 243 | var nodes = jp.nodes(partial, '$..*').slice(1);
|
|---|
| 244 |
|
|---|
| 245 | nodes.forEach(function(node) {
|
|---|
| 246 | if (results.length >= count) return;
|
|---|
| 247 | component.expression.value.forEach(function(component) {
|
|---|
| 248 | var _component = { operation: 'subscript', scope: 'child', expression: component.expression };
|
|---|
| 249 | var handler = self.resolve(_component);
|
|---|
| 250 | var _results = handler(_component, node);
|
|---|
| 251 | results = results.concat(_results);
|
|---|
| 252 | });
|
|---|
| 253 | });
|
|---|
| 254 |
|
|---|
| 255 | return unique(results);
|
|---|
| 256 | },
|
|---|
| 257 |
|
|---|
| 258 | 'subscript-child-filter_expression': function(component, partial, count) {
|
|---|
| 259 |
|
|---|
| 260 | // slice out the expression from ?(expression)
|
|---|
| 261 | var src = component.expression.value.slice(2, -1);
|
|---|
| 262 | var ast = aesprim.parse(src).body[0].expression;
|
|---|
| 263 |
|
|---|
| 264 | var passable = function(key, value) {
|
|---|
| 265 | return evaluate(ast, { '@': value });
|
|---|
| 266 | }
|
|---|
| 267 |
|
|---|
| 268 | return this.descend(partial, null, passable, count);
|
|---|
| 269 |
|
|---|
| 270 | },
|
|---|
| 271 |
|
|---|
| 272 | 'subscript-descendant-filter_expression': function(component, partial, count) {
|
|---|
| 273 |
|
|---|
| 274 | // slice out the expression from ?(expression)
|
|---|
| 275 | var src = component.expression.value.slice(2, -1);
|
|---|
| 276 | var ast = aesprim.parse(src).body[0].expression;
|
|---|
| 277 |
|
|---|
| 278 | var passable = function(key, value) {
|
|---|
| 279 | return evaluate(ast, { '@': value });
|
|---|
| 280 | }
|
|---|
| 281 |
|
|---|
| 282 | return this.traverse(partial, null, passable, count);
|
|---|
| 283 | },
|
|---|
| 284 |
|
|---|
| 285 | 'subscript-child-script_expression': function(component, partial) {
|
|---|
| 286 | var exp = component.expression.value.slice(1, -1);
|
|---|
| 287 | return eval_recurse(partial, exp, '$[{{value}}]');
|
|---|
| 288 | },
|
|---|
| 289 |
|
|---|
| 290 | 'member-child-script_expression': function(component, partial) {
|
|---|
| 291 | var exp = component.expression.value.slice(1, -1);
|
|---|
| 292 | return eval_recurse(partial, exp, '$.{{value}}');
|
|---|
| 293 | },
|
|---|
| 294 |
|
|---|
| 295 | 'member-descendant-script_expression': function(component, partial) {
|
|---|
| 296 | var exp = component.expression.value.slice(1, -1);
|
|---|
| 297 | return eval_recurse(partial, exp, '$..value');
|
|---|
| 298 | }
|
|---|
| 299 | };
|
|---|
| 300 |
|
|---|
| 301 | Handlers.prototype._fns['subscript-child-string_literal'] =
|
|---|
| 302 | Handlers.prototype._fns['member-child-identifier'];
|
|---|
| 303 |
|
|---|
| 304 | Handlers.prototype._fns['member-descendant-numeric_literal'] =
|
|---|
| 305 | Handlers.prototype._fns['subscript-descendant-string_literal'] =
|
|---|
| 306 | Handlers.prototype._fns['member-descendant-identifier'];
|
|---|
| 307 |
|
|---|
| 308 | function eval_recurse(partial, src, template) {
|
|---|
| 309 |
|
|---|
| 310 | var jp = require('./index');
|
|---|
| 311 | var ast = aesprim.parse(src).body[0].expression;
|
|---|
| 312 | var value = evaluate(ast, { '@': partial.value });
|
|---|
| 313 | var path = template.replace(/\{\{\s*value\s*\}\}/g, value);
|
|---|
| 314 |
|
|---|
| 315 | var results = jp.nodes(partial.value, path);
|
|---|
| 316 | results.forEach(function(r) {
|
|---|
| 317 | r.path = partial.path.concat(r.path.slice(1));
|
|---|
| 318 | });
|
|---|
| 319 |
|
|---|
| 320 | return results;
|
|---|
| 321 | }
|
|---|
| 322 |
|
|---|
| 323 | function is_array(val) {
|
|---|
| 324 | return Array.isArray(val);
|
|---|
| 325 | }
|
|---|
| 326 |
|
|---|
| 327 | function is_object(val) {
|
|---|
| 328 | // is this a non-array, non-null object?
|
|---|
| 329 | return val && !(val instanceof Array) && val instanceof Object;
|
|---|
| 330 | }
|
|---|
| 331 |
|
|---|
| 332 | function traverser(recurse) {
|
|---|
| 333 |
|
|---|
| 334 | return function(partial, ref, passable, count) {
|
|---|
| 335 |
|
|---|
| 336 | var value = partial.value;
|
|---|
| 337 | var path = partial.path;
|
|---|
| 338 |
|
|---|
| 339 | var results = [];
|
|---|
| 340 |
|
|---|
| 341 | var descend = function(value, path) {
|
|---|
| 342 |
|
|---|
| 343 | if (is_array(value)) {
|
|---|
| 344 | value.forEach(function(element, index) {
|
|---|
| 345 | if (results.length >= count) { return }
|
|---|
| 346 | if (passable(index, element, ref)) {
|
|---|
| 347 | results.push({ path: path.concat(index), value: element });
|
|---|
| 348 | }
|
|---|
| 349 | });
|
|---|
| 350 | value.forEach(function(element, index) {
|
|---|
| 351 | if (results.length >= count) { return }
|
|---|
| 352 | if (recurse) {
|
|---|
| 353 | descend(element, path.concat(index));
|
|---|
| 354 | }
|
|---|
| 355 | });
|
|---|
| 356 | } else if (is_object(value)) {
|
|---|
| 357 | this.keys(value).forEach(function(k) {
|
|---|
| 358 | if (results.length >= count) { return }
|
|---|
| 359 | if (passable(k, value[k], ref)) {
|
|---|
| 360 | results.push({ path: path.concat(k), value: value[k] });
|
|---|
| 361 | }
|
|---|
| 362 | })
|
|---|
| 363 | this.keys(value).forEach(function(k) {
|
|---|
| 364 | if (results.length >= count) { return }
|
|---|
| 365 | if (recurse) {
|
|---|
| 366 | descend(value[k], path.concat(k));
|
|---|
| 367 | }
|
|---|
| 368 | });
|
|---|
| 369 | }
|
|---|
| 370 | }.bind(this);
|
|---|
| 371 | descend(value, path);
|
|---|
| 372 | return results;
|
|---|
| 373 | }
|
|---|
| 374 | }
|
|---|
| 375 |
|
|---|
| 376 | function _descend(passable) {
|
|---|
| 377 | return function(component, partial, count) {
|
|---|
| 378 | return this.descend(partial, component.expression.value, passable, count);
|
|---|
| 379 | }
|
|---|
| 380 | }
|
|---|
| 381 |
|
|---|
| 382 | function _traverse(passable) {
|
|---|
| 383 | return function(component, partial, count) {
|
|---|
| 384 | return this.traverse(partial, component.expression.value, passable, count);
|
|---|
| 385 | }
|
|---|
| 386 | }
|
|---|
| 387 |
|
|---|
| 388 | function evaluate(ast, scope) {
|
|---|
| 389 | if (!isSafeAst(ast)) {
|
|---|
| 390 | throw new Error('Unsafe expression: script and filter expressions may only access the current node (@) with safe property names');
|
|---|
| 391 | }
|
|---|
| 392 | try { return _evaluate(ast, scope) }
|
|---|
| 393 | catch (e) { }
|
|---|
| 394 | }
|
|---|
| 395 |
|
|---|
| 396 | function unique(results) {
|
|---|
| 397 | results = results.filter(function(d) { return d })
|
|---|
| 398 | return _uniq(
|
|---|
| 399 | results,
|
|---|
| 400 | function(r) { return r.path.map(function(c) { return String(c).replace('-', '--') }).join('-') }
|
|---|
| 401 | );
|
|---|
| 402 | }
|
|---|
| 403 |
|
|---|
| 404 | function _parse_nullable_int(val) {
|
|---|
| 405 | var sval = String(val);
|
|---|
| 406 | return sval.match(/^-?[0-9]+$/) ? parseInt(sval) : null;
|
|---|
| 407 | }
|
|---|
| 408 |
|
|---|
| 409 | module.exports = Handlers;
|
|---|