Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ JSONPath.prototype.parent = function(obj, string) {
assert.ok(string, "we need a path");

var node = this.nodes(obj, string)[0];
if (node) this._assert_safe_path_keys(node.path);
var key = node.path.pop(); /* jshint unused:false */
return this.value(obj, node.path);
}
Expand All @@ -39,6 +40,7 @@ JSONPath.prototype.apply = function(obj, string, fn) {
});

nodes.forEach(function(node) {
this._assert_safe_path_keys(node.path);
var key = node.path.pop();
var parent = this.value(obj, this.stringify(node.path));
var val = node.value = fn.call(obj, parent[key]);
Expand All @@ -56,6 +58,7 @@ JSONPath.prototype.value = function(obj, path, value) {
if (arguments.length >= 3) {
var node = this.nodes(obj, path).shift();
if (!node) return this._vivify(obj, path, value);
this._assert_safe_path_keys(node.path);
var key = node.path.slice(-1).shift();
var parent = this.parent(obj, this.stringify(node.path));
parent[key] = value;
Expand All @@ -73,13 +76,16 @@ JSONPath.prototype._vivify = function(obj, string, value) {
var path = this.parser.parse(string)
.map(function(component) { return component.expression.value });

this._assert_safe_path_keys(path);

var setValue = function(path, value) {
var key = path.pop();
var node = self.value(obj, path);
if (!node) {
setValue(path.concat(), typeof key === 'string' ? {} : []);
node = self.value(obj, path);
}
self._assert_safe_key(key);
node[key] = value;
}
setValue(path, value);
Expand Down Expand Up @@ -116,6 +122,7 @@ JSONPath.prototype.nodes = function(obj, string, count) {
if (count === 0) return [];

var path = this.parser.parse(string);
this._assert_safe_components(path);
var handlers = this.handlers;

var partials = [ { path: ['$'], value: obj } ];
Expand Down Expand Up @@ -206,6 +213,7 @@ JSONPath.prototype._normalize = function(path) {
if (component == '$' && index === 0) return;

if (typeof component == "string" && component.match("^" + dict.identifier + "$")) {
this._assert_safe_key(component);

_path.push({
operation: 'member',
Expand All @@ -218,13 +226,15 @@ JSONPath.prototype._normalize = function(path) {
var type = typeof component == "number" ?
'numeric_literal' : 'string_literal';

if (type === 'string_literal') this._assert_safe_key(component);

_path.push({
operation: 'subscript',
scope: 'child',
expression: { value: component, type: type }
});
}
});
}, this);

return _path;

Expand All @@ -236,10 +246,55 @@ JSONPath.prototype._normalize = function(path) {
throw new Error("couldn't understand path " + path);
}

JSONPath.prototype._assert_safe_key = function(key) {
if (_is_unsafe_key(key)) {
throw new Error("Unsafe key in JSONPath: " + key);
}
}

JSONPath.prototype._assert_safe_path_keys = function(path) {
if (!path || !path.forEach) return;
path.forEach(function(key) {
if (key === '$') return;
if (typeof key === 'string') this._assert_safe_key(key);
}, this);
}

JSONPath.prototype._assert_safe_components = function(components) {
var self = this;
if (!components || !components.forEach) return;

var checkExpression = function(expression) {
if (!expression) return;
if (expression.type === 'identifier' || expression.type === 'string_literal') {
self._assert_safe_key(expression.value);
return;
}

if (expression.type === 'union' && Array.isArray(expression.value)) {
expression.value.forEach(function(component) {
if (component && component.expression) {
checkExpression(component.expression);
}
});
}
};

components.forEach(function(component) {
if (component && component.expression) {
checkExpression(component.expression);
}
});
}

function _is_string(obj) {
return Object.prototype.toString.call(obj) == '[object String]';
}

function _is_unsafe_key(key) {
return key === '__proto__' || key === 'prototype' || key === 'constructor';
}

JSONPath.Handlers = Handlers;
JSONPath.Parser = Parser;

Expand Down
51 changes: 51 additions & 0 deletions test/security.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
var assert = require('assert');
var jp = require('../');

suite('security', function() {

var cleanup = function() {
if (Object.prototype.polluted) {
delete Object.prototype.polluted;
}
};

teardown(function() {
cleanup();
});

test('blocks prototype pollution via value()', function() {
cleanup();
var data = {};
assert.throws(function() {
jp.value(data, '$.__proto__.polluted', 'yes');
}, /Unsafe key/);
assert.equal(({}).polluted, undefined);
});

test('blocks prototype pollution via apply()', function() {
cleanup();
var data = { safe: { ok: true } };
assert.throws(function() {
jp.apply(data, '$.__proto__.polluted', function() { return 'yes'; });
}, /Unsafe key/);
assert.equal(({}).polluted, undefined);
});

test('blocks unsafe subscript access', function() {
cleanup();
var data = {};
assert.throws(function() {
jp.query(data, '$["__proto__"]["polluted"]');
}, /Unsafe key/);
assert.equal(({}).polluted, undefined);
});

test('blocks unsafe union access', function() {
cleanup();
var data = { safe: 1 };
assert.throws(function() {
jp.nodes(data, "$['safe','__proto__']");
}, /Unsafe key/);
assert.equal(({}).polluted, undefined);
});
});