source: frontend/node_modules/jsonpath/lib/handlers.js@ 9af201e

Last change on this file since 9af201e was 9af201e, checked in by MBK <marija.karapandzova@…>, 11 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 11.0 KB
Line 
1var aesprim = require('./aesprim');
2var slice = require('./slice');
3var _evaluate = require('static-eval');
4var _uniq = require('underscore').uniq;
5
6// Property names that must never be accessible in expressions.
7// Mitigates prototype pollution and constructor escape attacks.
8var UNSAFE_PROPERTY_NAMES = Object.create(null);
9
10/* jshint -W069: true */
11UNSAFE_PROPERTY_NAMES['constructor'] = true;
12UNSAFE_PROPERTY_NAMES['__proto__'] = true;
13UNSAFE_PROPERTY_NAMES['prototype'] = true;
14/* jshint -W069: false */
15
16function isUnsafePropertyName(name) {
17 return typeof name === 'string' && UNSAFE_PROPERTY_NAMES[name] === true;
18}
19
20function 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
152var Handlers = function() {
153 return this.initialize.apply(this, arguments);
154}
155
156Handlers.prototype.initialize = function() {
157 this.traverse = traverser(true);
158 this.descend = traverser();
159}
160
161Handlers.prototype.keys = Object.keys;
162
163Handlers.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
172Handlers.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
181Handlers.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
301Handlers.prototype._fns['subscript-child-string_literal'] =
302 Handlers.prototype._fns['member-child-identifier'];
303
304Handlers.prototype._fns['member-descendant-numeric_literal'] =
305 Handlers.prototype._fns['subscript-descendant-string_literal'] =
306 Handlers.prototype._fns['member-descendant-identifier'];
307
308function 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
323function is_array(val) {
324 return Array.isArray(val);
325}
326
327function is_object(val) {
328 // is this a non-array, non-null object?
329 return val && !(val instanceof Array) && val instanceof Object;
330}
331
332function 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
376function _descend(passable) {
377 return function(component, partial, count) {
378 return this.descend(partial, component.expression.value, passable, count);
379 }
380}
381
382function _traverse(passable) {
383 return function(component, partial, count) {
384 return this.traverse(partial, component.expression.value, passable, count);
385 }
386}
387
388function 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
396function 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
404function _parse_nullable_int(val) {
405 var sval = String(val);
406 return sval.match(/^-?[0-9]+$/) ? parseInt(sval) : null;
407}
408
409module.exports = Handlers;
Note: See TracBrowser for help on using the repository browser.