Skip to content

Commit c62ba21

Browse files
Heath Morrisonbajtos
authored andcommitted
Coerce array-like objects into arrays
The query-string parser used by express https://github.com/ljharb/qs#parsing-arrays limits the size of arrays that are created from query strings to 20 items. Arrays larger than that are converted to objects using numeric indices. This commit fixes the coercion algorithm used by queries to treat number-indexed objects as arrays. We still maintain a strict understanding of an "array-like object" to limit the opportunity for subtle bugs. In particular, the presence of non-index keys is an indication that the object was not intended to be interpreted as an array.
1 parent 553c944 commit c62ba21

File tree

2 files changed

+103
-13
lines changed

2 files changed

+103
-13
lines changed

lib/dao.js

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1537,6 +1537,27 @@ function NumberType(val) {
15371537
return !isNaN(num) ? num : val;
15381538
}
15391539

1540+
function coerceArray(val) {
1541+
if (Array.isArray(val)) {
1542+
return val;
1543+
}
1544+
1545+
if (!utils.isPlainObject(val)) {
1546+
throw new Error(g.f('Value is not an {{array}} or {{object}} with sequential numeric indices'));
1547+
}
1548+
1549+
var arrayVal = new Array(Object.keys(val).length);
1550+
for (var i = 0; i < arrayVal.length; ++i) {
1551+
if (!val.hasOwnProperty(i)) {
1552+
throw new Error(g.f('Value is not an {{array}} or {{object}} with sequential numeric indices'));
1553+
}
1554+
1555+
arrayVal[i] = val[i];
1556+
}
1557+
1558+
return arrayVal;
1559+
}
1560+
15401561
/*
15411562
* Coerce values based the property types
15421563
* @param {Object} where The where clause
@@ -1561,16 +1582,18 @@ DataAccessObject._coerce = function(where) {
15611582
// Handle logical operators
15621583
if (p === 'and' || p === 'or' || p === 'nor') {
15631584
var clauses = where[p];
1564-
if (Array.isArray(clauses)) {
1565-
for (var k = 0; k < clauses.length; k++) {
1566-
self._coerce(clauses[k]);
1567-
}
1568-
} else {
1569-
err = new Error(g.f('The %s operator has invalid clauses %j', p, clauses));
1585+
try {
1586+
clauses = coerceArray(clauses);
1587+
} catch (e) {
1588+
err = new Error(g.f('The %s operator has invalid clauses %j: %s', p, clauses, e.message));
15701589
err.statusCode = 400;
15711590
throw err;
15721591
}
15731592

1593+
for (var k = 0; k < clauses.length; k++) {
1594+
self._coerce(clauses[k]);
1595+
}
1596+
15741597
continue;
15751598
}
15761599
var DataType = props[p] && props[p].type;
@@ -1625,15 +1648,21 @@ DataAccessObject._coerce = function(where) {
16251648
switch (operator) {
16261649
case 'inq':
16271650
case 'nin':
1628-
if (!Array.isArray(val)) {
1629-
err = new Error(g.f('The %s property has invalid clause %j', p, where[p]));
1651+
case 'between':
1652+
try {
1653+
val = coerceArray(val);
1654+
} catch (e) {
1655+
err = new Error(g.f('The %s property has invalid clause %j: %s', p, where[p], e));
16301656
err.statusCode = 400;
16311657
throw err;
16321658
}
1633-
break;
1634-
case 'between':
1635-
if (!Array.isArray(val) || val.length !== 2) {
1636-
err = new Error(g.f('The %s property has invalid clause %j', p, where[p]));
1659+
1660+
if (operator === 'between' && val.length !== 2) {
1661+
err = new Error(g.f(
1662+
'The %s property has invalid clause %j: Expected precisely 2 values, received %d',
1663+
p,
1664+
where[p],
1665+
val.length));
16371666
err.statusCode = 400;
16381667
throw err;
16391668
}
@@ -1643,7 +1672,10 @@ DataAccessObject._coerce = function(where) {
16431672
case 'ilike':
16441673
case 'nilike':
16451674
if (!(typeof val === 'string' || val instanceof RegExp)) {
1646-
err = new Error(g.f('The %s property has invalid clause %j', p, where[p]));
1675+
err = new Error(g.f(
1676+
'The %s property has invalid clause %j: Expected a string or RegExp',
1677+
p,
1678+
where[p]));
16471679
err.statusCode = 400;
16481680
throw err;
16491681
}
@@ -1660,6 +1692,14 @@ DataAccessObject._coerce = function(where) {
16601692
}
16611693
}
16621694
}
1695+
1696+
try {
1697+
// Coerce val into an array if it resembles an array-like object
1698+
val = coerceArray(val);
1699+
} catch (e) {
1700+
// NOOP when not coercable into an array.
1701+
}
1702+
16631703
// Coerce the array items
16641704
if (Array.isArray(val)) {
16651705
for (var i = 0; i < val.length; i++) {

test/loopback-dl.test.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,6 +1476,56 @@ describe('DataAccessObject', function() {
14761476
assert.deepEqual(where, {and: [{age: 10}], vip: true});
14771477
});
14781478

1479+
var COERCIONS = [
1480+
{
1481+
in: {scores: {0: '10', 1: '20'}},
1482+
out: {scores: [10, 20]},
1483+
},
1484+
{
1485+
in: {and: {0: {age: '10'}, 1: {vip: 'true'}}},
1486+
out: {and: [{age: 10}, {vip: true}]},
1487+
},
1488+
{
1489+
in: {or: {0: {age: '10'}, 1: {vip: 'true'}}},
1490+
out: {or: [{age: 10}, {vip: true}]},
1491+
},
1492+
{
1493+
in: {id: {inq: {0: 'aaa', 1: 'bbb'}}},
1494+
out: {id: {inq: ['aaa', 'bbb']}},
1495+
},
1496+
{
1497+
in: {id: {nin: {0: 'aaa', 1: 'bbb'}}},
1498+
out: {id: {nin: ['aaa', 'bbb']}},
1499+
},
1500+
{
1501+
in: {scores: {between: {0: '0', 1: '42'}}},
1502+
out: {scores: {between: [0, 42]}},
1503+
},
1504+
];
1505+
1506+
COERCIONS.forEach(function(coercion) {
1507+
var inStr = JSON.stringify(coercion.in);
1508+
it('coerces where clause with array-like objects ' + inStr, function() {
1509+
assert.deepEqual(model._coerce(coercion.in), coercion.out);
1510+
});
1511+
});
1512+
1513+
var INVALID_CLAUSES = [
1514+
{scores: {inq: {0: '10', 1: '20', 4: '30'}}},
1515+
{scores: {inq: {0: '10', 1: '20', bogus: 'true'}}},
1516+
{scores: {between: {0: '10', 1: '20', 2: '30'}}},
1517+
];
1518+
1519+
INVALID_CLAUSES.forEach(function(where) {
1520+
var whereStr = JSON.stringify(where);
1521+
it('throws an error on malformed array-like object ' + whereStr,
1522+
function() {
1523+
assert.throws(
1524+
function() { model._coerce(where); },
1525+
/property has invalid clause/);
1526+
});
1527+
});
1528+
14791529
it('throws an error if the where property is not an object', function() {
14801530
try {
14811531
// The where clause has to be an object

0 commit comments

Comments
 (0)