update
This commit is contained in:
+365
@@ -14,6 +14,12 @@ var hasProto = require('has-proto')();
|
||||
var qs = require('../');
|
||||
var utils = require('../lib/utils');
|
||||
|
||||
var characterizeParse = function characterizeParse(st, input, opts, expected, label) {
|
||||
var result;
|
||||
st.doesNotThrow(function () { result = qs.parse(input, opts); }, label + ': does not throw');
|
||||
st.deepEqual(result, expected, label + ': parses to the current lenient output');
|
||||
};
|
||||
|
||||
test('parse()', function (t) {
|
||||
t.test('parses a simple string', function (st) {
|
||||
st.deepEqual(qs.parse('0=foo'), { 0: 'foo' });
|
||||
@@ -210,6 +216,21 @@ test('parse()', function (t) {
|
||||
t.test('uses original key when depth = 0', function (st) {
|
||||
st.deepEqual(qs.parse('a[0]=b&a[1]=c', { depth: 0 }), { 'a[0]': 'b', 'a[1]': 'c' });
|
||||
st.deepEqual(qs.parse('a[0][0]=b&a[0][1]=c&a[1]=d&e=2', { depth: 0 }), { 'a[0][0]': 'b', 'a[0][1]': 'c', 'a[1]': 'd', e: '2' });
|
||||
st.deepEqual(qs.parse('a.b=c', { depth: 0, allowDots: true }), { 'a[b]': 'c' }, 'normalizes dots before applying depth-0 behavior');
|
||||
st.deepEqual(qs.parse('toString=foo', { depth: 0 }), {}, 'respects prototype guard at depth 0');
|
||||
st.deepEqual(qs.parse('toString=foo', { depth: 0, allowPrototypes: true }), { toString: 'foo' }, 'allows prototypes at depth 0 when enabled');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('ignores prototype keys when depth = 0 and allowPrototypes is false', function (st) {
|
||||
st.deepEqual(qs.parse('toString=foo', { depth: 0 }), {});
|
||||
st.deepEqual(qs.parse('hasOwnProperty=bar', { depth: 0 }), {});
|
||||
st.deepEqual(qs.parse('toString=foo&a=b', { depth: 0 }), { a: 'b' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('allows prototype keys when depth = 0 and allowPrototypes is true', function (st) {
|
||||
st.deepEqual(qs.parse('toString=foo', { depth: 0, allowPrototypes: true }), { toString: 'foo' });
|
||||
st.end();
|
||||
});
|
||||
|
||||
@@ -251,6 +272,94 @@ test('parse()', function (t) {
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('parses keys with literal [] inside a bracket group (#493)', function (st) {
|
||||
// A bracket pair inside a bracket group should be treated literally as part of the key
|
||||
st.deepEqual(
|
||||
qs.parse('search[withbracket[]]=foobar'),
|
||||
{ search: { 'withbracket[]': 'foobar' } },
|
||||
'treats inner [] literally when inside a bracket group'
|
||||
);
|
||||
|
||||
// Single-level variant
|
||||
st.deepEqual(
|
||||
qs.parse('a[b[]]=c'),
|
||||
{ a: { 'b[]': 'c' } },
|
||||
'keeps "b[]" as a literal key'
|
||||
);
|
||||
|
||||
// Nested with an array push on the outer level
|
||||
st.deepEqual(
|
||||
qs.parse('list[][x[]]=y'),
|
||||
{ list: [{ 'x[]': 'y' }] },
|
||||
'preserves inner [] while still treating outer [] as array push'
|
||||
);
|
||||
|
||||
// Multiple nested bracket pairs: inner [] remains literal as part of the key
|
||||
st.deepEqual(
|
||||
qs.parse('a[b[c[]]]=d'),
|
||||
{ a: { 'b[c[]]': 'd' } },
|
||||
'treats "b[c[]]" as a literal key inside the bracket group'
|
||||
);
|
||||
|
||||
// Depth limits with literal brackets: preserve inner [] while limiting bracket-group parsing
|
||||
st.deepEqual(
|
||||
qs.parse('a[b[c[]]][d]=e', { depth: 1 }),
|
||||
{ a: { 'b[c[]]': { '[d]': 'e' } } },
|
||||
'respects depth: 1 and preserves literal inner [] in the parsed key'
|
||||
);
|
||||
|
||||
// Unterminated inner bracket group is wrapped as a literal remainder segment
|
||||
st.deepEqual(
|
||||
qs.parse('a[[]b=c'),
|
||||
{ a: { '[[]b': 'c' } },
|
||||
'handles unterminated inner bracket groups without throwing'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('currently parses unbalanced bracket keys after a parent leniently to literal segments (issue #558)', function (st) {
|
||||
characterizeParse(st, 'a[bc=v', undefined, { a: { '[bc': 'v' } }, 'unclosed group after a parent');
|
||||
characterizeParse(st, 'a[=v', undefined, { a: { '[': 'v' } }, 'bare unclosed bracket after a parent');
|
||||
characterizeParse(st, 'a[b][c=v', undefined, { a: { b: { '[c': 'v' } } }, 'unclosed group after a valid one');
|
||||
characterizeParse(st, 'a[b]c[d=v', undefined, { a: { b: { '[d': 'v' } } }, 'unclosed group after text following a valid one');
|
||||
characterizeParse(st, 'filters[customtags:Env: Prod=v', undefined, { filters: { '[customtags:Env: Prod': 'v' } }, 'the issue #558 reproduction');
|
||||
characterizeParse(st, '][a=v', undefined, { ']': { '[a': 'v' } }, 'stray close bracket before an unclosed group');
|
||||
characterizeParse(st, 'a][b=v', undefined, { 'a]': { '[b': 'v' } }, 'stray close bracket inside the parent');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('currently parses unbalanced bracket keys containing inner brackets leniently (issue #558)', function (st) {
|
||||
characterizeParse(st, 'a[b[c=v', undefined, { a: { '[b[c': 'v' } }, 'unclosed group containing an inner bracket');
|
||||
characterizeParse(st, 'a[b[c]=v', undefined, { a: { '[b[c]': 'v' } }, 'unbalanced group with an inner bracket and one close');
|
||||
characterizeParse(st, 'a[b][c[d=v', undefined, { a: { b: { '[c[d': 'v' } } }, 'unclosed inner-bracket group after a valid one');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('currently parses bracket-prefixed unbalanced keys leniently (issue #558)', function (st) {
|
||||
characterizeParse(st, '[abc=v', undefined, { '[abc': 'v' }, 'key starting with an unclosed bracket');
|
||||
characterizeParse(st, '[[]b=v', undefined, { '[[]b': 'v' }, 'key starting with an unbalanced bracket group');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('lenient unbalanced-bracket handling currently depends on the depth option (issue #558)', function (st) {
|
||||
characterizeParse(st, 'a[b]c[d]e[f=v', { depth: 5 }, { a: { b: { d: { '[f': 'v' } } } }, 'consumes groups up to the depth budget then keeps the unclosed remainder literal');
|
||||
characterizeParse(st, 'a[b]c[d]e[f=v', { depth: 1 }, { a: { b: { '[d]e[f': 'v' } } }, 'a lower depth keeps more of the unclosed remainder literal');
|
||||
characterizeParse(st, 'a[bc=v', { depth: 0 }, { 'a[bc': 'v' }, 'depth 0 keeps the entire key literal');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('currently parses an allowDots key with a trailing unclosed bracket leniently (issue #558)', function (st) {
|
||||
characterizeParse(st, 'a.b[c=v', { allowDots: true }, { a: { b: { '[c': 'v' } } }, 'allowDots expands the dot then keeps the unclosed bracket literal');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('valid and stray-close bracket keys are unaffected by unbalanced-bracket handling', function (st) {
|
||||
characterizeParse(st, 'a]b=v', undefined, { 'a]b': 'v' }, 'stray close bracket with no open bracket stays a flat key');
|
||||
characterizeParse(st, 'a[b]extra=v', undefined, { a: { b: 'v' } }, 'text after a balanced group is ignored');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('allows to specify array indices', function (st) {
|
||||
st.deepEqual(qs.parse('a[1]=c&a[0]=b&a[2]=d'), { a: ['b', 'c', 'd'] });
|
||||
st.deepEqual(qs.parse('a[1]=c&a[0]=b'), { a: ['b', 'c'] });
|
||||
@@ -665,6 +774,21 @@ test('parse()', function (t) {
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not crash on multi-step circular references', function (st) {
|
||||
var a = {};
|
||||
a.b = { c: { d: a } };
|
||||
|
||||
var parsed;
|
||||
|
||||
st.doesNotThrow(function () {
|
||||
parsed = qs.parse({ foo: a });
|
||||
});
|
||||
|
||||
st.equal('foo' in parsed, true, 'parsed has "foo" property');
|
||||
st.equal(parsed.foo.b.c.d, parsed.foo, 'the multi-step cycle is preserved');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('does not crash when parsing deep objects', function (st) {
|
||||
var parsed;
|
||||
var str = 'foo';
|
||||
@@ -898,6 +1022,22 @@ test('parse()', function (t) {
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('object-valued input with own `__proto__` does not mutate sub-object [[Prototype]]', function (st) {
|
||||
// JSON.parse creates own data `__proto__` properties (via CreateDataProperty),
|
||||
// which would trigger the Object.prototype.__proto__ accessor if merged via `acc[key] = value`.
|
||||
var out = qs.parse({
|
||||
'user[name]': 'alice',
|
||||
user: JSON.parse('{"__proto__":{"isAdmin":true}}')
|
||||
}, { allowPrototypes: false });
|
||||
|
||||
st.equal(out.user.name, 'alice', 'name from bracket key is preserved');
|
||||
st.equal(out.user.isAdmin, undefined, 'attacker-controlled inherited property is not exposed');
|
||||
st.equal(Object.getPrototypeOf(out.user), Object.prototype, 'sub-object [[Prototype]] is unchanged');
|
||||
st.equal(Object.prototype.isAdmin, undefined, 'Object.prototype is not polluted');
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('can return null objects', { skip: !hasProto }, function (st) {
|
||||
var expected = {
|
||||
__proto__: null,
|
||||
@@ -1074,6 +1214,15 @@ test('parse()', function (t) {
|
||||
};
|
||||
|
||||
st.deepEqual(qs.parse('KeY=vAlUe', { decoder: decoder }), { key: 'VALUE' });
|
||||
|
||||
var noopDecoder = function () { return 'x'; };
|
||||
noopDecoder();
|
||||
st['throws'](
|
||||
function () { decoder('x', noopDecoder, 'utf-8', 'unknown'); },
|
||||
'this should never happen! type: unknown',
|
||||
'decoder throws for unexpected type'
|
||||
);
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
@@ -1103,6 +1252,14 @@ test('parse()', function (t) {
|
||||
new RangeError('Parameter limit exceeded. Only 3 parameters allowed.'),
|
||||
'throws error when parameter limit is exceeded'
|
||||
);
|
||||
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a=1&b=2', { parameterLimit: 1, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Parameter limit exceeded. Only 1 parameter allowed.'),
|
||||
'throws error with singular "parameter" when parameterLimit is 1'
|
||||
);
|
||||
sst.end();
|
||||
});
|
||||
|
||||
@@ -1124,6 +1281,12 @@ test('parse()', function (t) {
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('allows unlimited parameters when parameterLimit is Infinity and throwOnLimitExceeded is true', function (sst) {
|
||||
var result = qs.parse('a=1&b=2&c=3&d=4&e=5&f=6', { parameterLimit: Infinity, throwOnLimitExceeded: true });
|
||||
sst.deepEqual(result, { a: '1', b: '2', c: '3', d: '4', e: '5', f: '6' }, 'parses all parameters without truncation or throwing');
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
@@ -1189,6 +1352,14 @@ test('parse()', function (t) {
|
||||
'throws error when a sparse index exceeds arrayLimit'
|
||||
);
|
||||
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a[2]=b', { arrayLimit: 1, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'throws error with singular "element" when arrayLimit is 1'
|
||||
);
|
||||
|
||||
sst.end();
|
||||
});
|
||||
|
||||
@@ -1206,6 +1377,168 @@ test('parse()', function (t) {
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('throws when duplicate bracket keys exceed arrayLimit with throwOnLimitExceeded', function (sst) {
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a[]=1&a[]=2&a[]=3&a[]=4&a[]=5&a[]=6', { arrayLimit: 5, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
|
||||
'throws error when duplicate bracket notation exceeds array limit'
|
||||
);
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('throws when cumulative comma + duplicate-key combine exceeds arrayLimit', function (sst) {
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a=1,2,3&a=4,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
|
||||
'throws when comma groups within the limit cumulatively exceed it across duplicate keys'
|
||||
);
|
||||
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a=v,v,v,v,v&a=v,v,v,v,v&a=v,v,v,v,v', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
|
||||
'throws on a subsequent part once the cumulative array is already over the limit'
|
||||
);
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('throws when plain duplicate keys combine past arrayLimit at the boundary', function (sst) {
|
||||
sst['throws'](
|
||||
function () { qs.parse('a=x&a=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'duplicate scalar keys'
|
||||
);
|
||||
|
||||
sst['throws'](
|
||||
function () { qs.parse('a[]=x&a[]=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'duplicate bracket keys'
|
||||
);
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('throws when mixed index and key notation merge past arrayLimit', function (sst) {
|
||||
sst['throws'](
|
||||
function () { qs.parse('a=x&a[0]=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'scalar then index that overflows on merge'
|
||||
);
|
||||
|
||||
sst['throws'](
|
||||
function () { qs.parse('a[0]=1&a[1]=2&a=3', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'indexed array then scalar that overflows on merge'
|
||||
);
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('enforces arrayLimit on merge at the boundary, consistently with combine', function (sst) {
|
||||
sst['throws'](
|
||||
function () { qs.parse('a[0]=x&a=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'a trailing scalar merged into an at-limit array throws'
|
||||
);
|
||||
sst.deepEqual(
|
||||
qs.parse('a[0]=x&a=y', { arrayLimit: 1 }),
|
||||
{ a: { 0: 'x', 1: 'y' } },
|
||||
'and converts to an overflow object without throwOnLimitExceeded'
|
||||
);
|
||||
|
||||
sst['throws'](
|
||||
function () { qs.parse('a[0]=x&a[]=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'mixed index and bracket notation merged past the limit throws'
|
||||
);
|
||||
sst.deepEqual(
|
||||
qs.parse('a[0]=x&a[]=y', { arrayLimit: 1 }),
|
||||
{ a: { 0: 'x', 1: 'y' } },
|
||||
'mixed index and bracket notation converts like duplicate-bracket combine'
|
||||
);
|
||||
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('does not throw when cumulative comma combine stays within arrayLimit', function (sst) {
|
||||
var result = qs.parse('a=1,2,3&a=4', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
||||
sst.deepEqual(result, { a: ['1', '2', '3', '4'] }, 'combined array within limit is preserved');
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('silently combines to an overflow object when throwOnLimitExceeded is not set', function (sst) {
|
||||
var result = qs.parse('a=1,2,3&a=4,5,6', { comma: true, arrayLimit: 5 });
|
||||
sst.deepEqual(result, { a: { 0: '1', 1: '2', 2: '3', 3: '4', 4: '5', 5: '6' } }, 'converts to object without throwing');
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('does not throw for comma groups nested under bracket notation, counting each group as one element', function (sst) {
|
||||
var result = qs.parse('a[]=1,2,3&a[]=4,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
||||
sst.deepEqual(result, { a: [['1', '2', '3'], ['4', '5', '6']] }, 'nested comma groups count as one element each');
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('throws before splitting when a single comma value exceeds arrayLimit', function (sst) {
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a=1,2,3,4,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
|
||||
'a flat comma value over the limit throws'
|
||||
);
|
||||
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a=1,2', { comma: true, arrayLimit: 1, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'singular message at arrayLimit 1'
|
||||
);
|
||||
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a[b]=1,2,3,4,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 5 elements allowed in an array.'),
|
||||
'a non-bracket nested key comma value over the limit throws'
|
||||
);
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('does not throw for a single comma value within arrayLimit', function (sst) {
|
||||
sst.deepEqual(
|
||||
qs.parse('a=1,2,3', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true }),
|
||||
{ a: ['1', '2', '3'] },
|
||||
'within the limit'
|
||||
);
|
||||
sst.deepEqual(
|
||||
qs.parse('a=1,2,3,4,5', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true }),
|
||||
{ a: ['1', '2', '3', '4', '5'] },
|
||||
'exactly at the limit'
|
||||
);
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('does not throw for a bracketed comma group within arrayLimit', function (sst) {
|
||||
var result = qs.parse('a[]=1,2,3,4,5,6', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
||||
sst.deepEqual(result, { a: [['1', '2', '3', '4', '5', '6']] }, 'a bracketed comma group is a single element');
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.test('throws for a bracketed comma group when arrayLimit is 0', function (sst) {
|
||||
sst['throws'](
|
||||
function () {
|
||||
qs.parse('a[]=1,2,3', { comma: true, arrayLimit: 0, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 0 elements allowed in an array.'),
|
||||
'a single bracketed element still exceeds arrayLimit 0'
|
||||
);
|
||||
sst.end();
|
||||
});
|
||||
|
||||
st.end();
|
||||
});
|
||||
|
||||
@@ -1462,6 +1795,14 @@ test('comma + arrayLimit', function (t) {
|
||||
new RangeError('Array limit exceeded. Only 3 elements allowed in an array.'),
|
||||
'throws error when comma-split exceeds array limit'
|
||||
);
|
||||
|
||||
st['throws'](
|
||||
function () {
|
||||
qs.parse('a=1,2,3', { comma: true, arrayLimit: 1, throwOnLimitExceeded: true });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'throws error with singular "element" when arrayLimit is 1'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
@@ -1564,5 +1905,29 @@ test('mixed array and object notation', function (t) {
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('uses existing array length for currentArrayLength when parsing object input with bracket keys', function (st) {
|
||||
var input = {};
|
||||
var arr = ['x', 'y'];
|
||||
arr.a = ['z', 'w'];
|
||||
input['a[]'] = arr;
|
||||
st.deepEqual(qs.parse(input), { a: ['x', 'y'] }, 'parses object input with bracket keys using existing array values');
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.test('throws with singular message when object input bracket key exceeds arrayLimit of 1', function (st) {
|
||||
var input = {};
|
||||
var arr = ['x'];
|
||||
arr.a = ['z', 'w'];
|
||||
input['a[]'] = arr;
|
||||
st['throws'](
|
||||
function () {
|
||||
qs.parse(input, { throwOnLimitExceeded: true, arrayLimit: 1 });
|
||||
},
|
||||
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
||||
'throws singular error for object input exceeding arrayLimit 1'
|
||||
);
|
||||
st.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user