Skip to content

Commit d065398

Browse files
committed
[Fix] parse: throw on unbalanced bracket groups
1 parent caf870c commit d065398

2 files changed

Lines changed: 58 additions & 35 deletions

File tree

lib/parse.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,20 @@ var parseObject = function (chain, val, options, valuesParsed) {
229229
// qs parse semantics for depth/prototype guards.
230230
var splitKeyIntoSegments = function splitKeyIntoSegments(originalKey, options) {
231231
var key = options.allowDots ? originalKey.replace(/\.([^.[]+)/g, '[$1]') : originalKey;
232+
var bracketLevel = 0;
233+
234+
for (var j = 0; j < key.length; ++j) {
235+
var code = key.charCodeAt(j);
236+
if (code === 0x5B) { // '['
237+
bracketLevel += 1;
238+
} else if (code === 0x5D && bracketLevel > 0) { // ']'
239+
bracketLevel -= 1;
240+
}
241+
}
242+
243+
if (bracketLevel > 0) {
244+
throw new RangeError('Unbalanced bracket group in query string key');
245+
}
232246

233247
// depth <= 0 keeps the whole key as one segment
234248
if (options.depth <= 0) {
@@ -280,10 +294,7 @@ var splitKeyIntoSegments = function splitKeyIntoSegments(originalKey, options) {
280294
}
281295

282296
if (close < 0) {
283-
// Unterminated group: wrap the raw remainder in one bracket pair so it stays
284-
// a single literal segment (e.g. "[[]b" -> "[[]b]"); we do not infer missing ']'.
285-
segments[segments.length] = '[' + key.slice(open) + ']';
286-
return segments;
297+
throw new RangeError('Unbalanced bracket group in query string key');
287298
}
288299

289300
var seg = key.slice(open, close + 1);

test/parse.js

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ var characterizeParse = function characterizeParse(st, input, opts, expected, la
2020
st.deepEqual(result, expected, label + ': parses to the current lenient output');
2121
};
2222

23+
var throwsUnbalancedBracket = function throwsUnbalancedBracket(st, input, opts, label) {
24+
st['throws'](
25+
function () { qs.parse(input, opts); },
26+
new RangeError('Unbalanced bracket group in query string key'),
27+
label + ': throws on the unbalanced bracket group'
28+
);
29+
};
30+
2331
test('parse()', function (t) {
2432
t.test('parses a simple string', function (st) {
2533
st.deepEqual(qs.parse('0=foo'), { 0: 'foo' });
@@ -308,49 +316,44 @@ test('parse()', function (t) {
308316
'respects depth: 1 and preserves literal inner [] in the parsed key'
309317
);
310318

311-
// Unterminated inner bracket group is wrapped as a literal remainder segment
312-
st.deepEqual(
313-
qs.parse('a[[]b=c'),
314-
{ a: { '[[]b': 'c' } },
315-
'handles unterminated inner bracket groups without throwing'
316-
);
319+
throwsUnbalancedBracket(st, 'a[[]b=c', undefined, 'throws on an unterminated inner bracket group');
317320

318321
st.end();
319322
});
320323

321-
t.test('currently parses unbalanced bracket keys after a parent leniently to literal segments (issue #558)', function (st) {
322-
characterizeParse(st, 'a[bc=v', undefined, { a: { '[bc': 'v' } }, 'unclosed group after a parent');
323-
characterizeParse(st, 'a[=v', undefined, { a: { '[': 'v' } }, 'bare unclosed bracket after a parent');
324-
characterizeParse(st, 'a[b][c=v', undefined, { a: { b: { '[c': 'v' } } }, 'unclosed group after a valid one');
325-
characterizeParse(st, 'a[b]c[d=v', undefined, { a: { b: { '[d': 'v' } } }, 'unclosed group after text following a valid one');
326-
characterizeParse(st, 'filters[customtags:Env: Prod=v', undefined, { filters: { '[customtags:Env: Prod': 'v' } }, 'the issue #558 reproduction');
327-
characterizeParse(st, '][a=v', undefined, { ']': { '[a': 'v' } }, 'stray close bracket before an unclosed group');
328-
characterizeParse(st, 'a][b=v', undefined, { 'a]': { '[b': 'v' } }, 'stray close bracket inside the parent');
324+
t.test('throws on unbalanced bracket keys after a parent (issue #558)', function (st) {
325+
throwsUnbalancedBracket(st, 'a[bc=v', undefined, 'unclosed group after a parent');
326+
throwsUnbalancedBracket(st, 'a[=v', undefined, 'bare unclosed bracket after a parent');
327+
throwsUnbalancedBracket(st, 'a[b][c=v', undefined, 'unclosed group after a valid one');
328+
throwsUnbalancedBracket(st, 'a[b]c[d=v', undefined, 'unclosed group after text following a valid one');
329+
throwsUnbalancedBracket(st, 'filters[customtags:Env: Prod=v', undefined, 'the issue #558 reproduction');
330+
throwsUnbalancedBracket(st, '][a=v', undefined, 'stray close bracket before an unclosed group');
331+
throwsUnbalancedBracket(st, 'a][b=v', undefined, 'stray close bracket inside the parent');
329332
st.end();
330333
});
331334

332-
t.test('currently parses unbalanced bracket keys containing inner brackets leniently (issue #558)', function (st) {
333-
characterizeParse(st, 'a[b[c=v', undefined, { a: { '[b[c': 'v' } }, 'unclosed group containing an inner bracket');
334-
characterizeParse(st, 'a[b[c]=v', undefined, { a: { '[b[c]': 'v' } }, 'unbalanced group with an inner bracket and one close');
335-
characterizeParse(st, 'a[b][c[d=v', undefined, { a: { b: { '[c[d': 'v' } } }, 'unclosed inner-bracket group after a valid one');
335+
t.test('throws on unbalanced bracket keys containing inner brackets (issue #558)', function (st) {
336+
throwsUnbalancedBracket(st, 'a[b[c=v', undefined, 'unclosed group containing an inner bracket');
337+
throwsUnbalancedBracket(st, 'a[b[c]=v', undefined, 'unbalanced group with an inner bracket and one close');
338+
throwsUnbalancedBracket(st, 'a[b][c[d=v', undefined, 'unclosed inner-bracket group after a valid one');
336339
st.end();
337340
});
338341

339-
t.test('currently parses bracket-prefixed unbalanced keys leniently (issue #558)', function (st) {
340-
characterizeParse(st, '[abc=v', undefined, { '[abc': 'v' }, 'key starting with an unclosed bracket');
341-
characterizeParse(st, '[[]b=v', undefined, { '[[]b': 'v' }, 'key starting with an unbalanced bracket group');
342+
t.test('throws on bracket-prefixed unbalanced keys (issue #558)', function (st) {
343+
throwsUnbalancedBracket(st, '[abc=v', undefined, 'key starting with an unclosed bracket');
344+
throwsUnbalancedBracket(st, '[[]b=v', undefined, 'key starting with an unbalanced bracket group');
342345
st.end();
343346
});
344347

345-
t.test('lenient unbalanced-bracket handling currently depends on the depth option (issue #558)', function (st) {
346-
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');
347-
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');
348-
characterizeParse(st, 'a[bc=v', { depth: 0 }, { 'a[bc': 'v' }, 'depth 0 keeps the entire key literal');
348+
t.test('throws on unbalanced brackets regardless of depth option (issue #558)', function (st) {
349+
throwsUnbalancedBracket(st, 'a[b]c[d]e[f=v', { depth: 5 }, 'unclosed group within the depth budget');
350+
throwsUnbalancedBracket(st, 'a[b]c[d]e[f=v', { depth: 1 }, 'unclosed group beyond the depth budget');
351+
throwsUnbalancedBracket(st, 'a[bc=v', { depth: 0 }, 'depth 0 still rejects an unclosed bracket group');
349352
st.end();
350353
});
351354

352-
t.test('currently parses an allowDots key with a trailing unclosed bracket leniently (issue #558)', function (st) {
353-
characterizeParse(st, 'a.b[c=v', { allowDots: true }, { a: { b: { '[c': 'v' } } }, 'allowDots expands the dot then keeps the unclosed bracket literal');
355+
t.test('throws on an allowDots key with a trailing unclosed bracket (issue #558)', function (st) {
356+
throwsUnbalancedBracket(st, 'a.b[c=v', { allowDots: true }, 'allowDots expands the dot before detecting the unclosed bracket');
354357
st.end();
355358
});
356359

@@ -899,10 +902,19 @@ test('parse()', function (t) {
899902
st.end();
900903
});
901904

902-
t.test('params starting with a starting bracket', function (st) {
903-
st.deepEqual(qs.parse('[=toString'), { '[': 'toString' });
904-
st.deepEqual(qs.parse('[[=toString'), { '[[': 'toString' });
905-
st.deepEqual(qs.parse('[hello[=toString'), { '[hello[': 'toString' });
905+
t.test('throws on params starting with an unbalanced starting bracket', function (st) {
906+
throwsUnbalancedBracket(st, '[=toString', undefined, 'single opening bracket');
907+
throwsUnbalancedBracket(st, '[[=toString', undefined, 'multiple opening brackets');
908+
throwsUnbalancedBracket(st, '[hello[=toString', undefined, 'opening brackets with text');
909+
st.end();
910+
});
911+
912+
t.test('throws on unterminated bracket groups after a parent key', function (st) {
913+
st['throws'](
914+
function () { qs.parse('filters[customtags:Env: Prod&startDate=2025-02-01'); },
915+
new RangeError('Unbalanced bracket group in query string key'),
916+
'throws on a missing closing bracket after a parent key'
917+
);
906918
st.end();
907919
});
908920

0 commit comments

Comments
 (0)