diff --git a/README.md b/README.md index 218f3e4c..1e0d9adc 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,18 @@ var deep = qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1 }); assert.deepEqual(deep, { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } }); ``` -The depth limit helps mitigate abuse when **qs** is used to parse user input, and it is recommended to keep it a reasonably small number. +You can configure **qs** to throw an error when parsing nested input beyond this depth using the `strictDepth` option (defaulted to false): + +```javascript +try { + qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1, strictDepth: true }); +} catch (err) { + assert(err instanceof RangeError); + assert.strictEqual(err.message, 'Input depth exceeded depth option of 1 and strictDepth is true'); +} +``` + +The depth limit helps mitigate abuse when **qs** is used to parse user input, and it is recommended to keep it a reasonably small number. The strictDepth option adds a layer of protection by throwing an error when the limit is exceeded, allowing you to catch and handle such cases. For similar reasons, by default **qs** will only parse up to 1000 parameters. This can be overridden by passing a `parameterLimit` option: diff --git a/lib/parse.js b/lib/parse.js index 0c6a6668..3d74c715 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -24,6 +24,7 @@ var defaults = { parameterLimit: 1000, parseArrays: true, plainObjects: false, + strictDepth: false, strictNullHandling: false }; @@ -201,9 +202,12 @@ var parseKeys = function parseQueryStringKeys(givenKey, val, options, valuesPars keys.push(segment[1]); } - // If there's a remainder, just add whatever is left + // If there's a remainder, check strictDepth option for throw, else just add whatever is left if (segment) { + if (options.strictDepth === true) { + throw new RangeError('Input depth exceeded depth option of ' + options.depth + ' and strictDepth is true'); + } keys.push('[' + key.slice(segment.index) + ']'); } @@ -260,6 +264,7 @@ var normalizeParseOptions = function normalizeParseOptions(opts) { parameterLimit: typeof opts.parameterLimit === 'number' ? opts.parameterLimit : defaults.parameterLimit, parseArrays: opts.parseArrays !== false, plainObjects: typeof opts.plainObjects === 'boolean' ? opts.plainObjects : defaults.plainObjects, + strictDepth: typeof opts.strictDepth === 'boolean' ? !!opts.strictDepth : defaults.strictDepth, strictNullHandling: typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling }; }; diff --git a/test/parse.js b/test/parse.js index 2bf4c411..8a2f487c 100644 --- a/test/parse.js +++ b/test/parse.js @@ -1068,3 +1068,103 @@ test('`duplicates` option', function (t) { t.end(); }); + +test('qs strictDepth option - throw cases', function (t) { + t.test('throws an exception when depth exceeds the limit with strictDepth: true', function (st) { + st['throws']( + function () { + qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1, strictDepth: true }); + }, + RangeError, + 'Should throw RangeError' + ); + st.end(); + }); + + t.test('throws an exception for multiple nested arrays with strictDepth: true', function (st) { + st['throws']( + function () { + qs.parse('a[0][1][2][3][4]=b', { depth: 3, strictDepth: true }); + }, + RangeError, + 'Should throw RangeError' + ); + st.end(); + }); + + t.test('throws an exception for nested objects and arrays with strictDepth: true', function (st) { + st['throws']( + function () { + qs.parse('a[b][c][0][d][e]=f', { depth: 3, strictDepth: true }); + }, + RangeError, + 'Should throw RangeError' + ); + st.end(); + }); + + t.test('throws an exception for different types of values with strictDepth: true', function (st) { + st['throws']( + function () { + qs.parse('a[b][c][d][e]=true&a[b][c][d][f]=42', { depth: 3, strictDepth: true }); + }, + RangeError, + 'Should throw RangeError' + ); + st.end(); + }); + +}); + +test('qs strictDepth option - non-throw cases', function (t) { + t.test('when depth is 0 and strictDepth true, do not throw', function (st) { + st.doesNotThrow( + function () { + qs.parse('a[b][c][d][e]=true&a[b][c][d][f]=42', { depth: 0, strictDepth: true }); + }, + RangeError, + 'Should not throw RangeError' + ); + st.end(); + }); + + t.test('parses successfully when depth is within the limit with strictDepth: true', function (st) { + st.doesNotThrow( + function () { + var result = qs.parse('a[b]=c', { depth: 1, strictDepth: true }); + st.deepEqual(result, { a: { b: 'c' } }, 'Should parse correctly'); + } + ); + st.end(); + }); + + t.test('does not throw an exception when depth exceeds the limit with strictDepth: false', function (st) { + st.doesNotThrow( + function () { + var result = qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1 }); + st.deepEqual(result, { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } }, 'Should parse with depth limit'); + } + ); + st.end(); + }); + + t.test('parses successfully when depth is within the limit with strictDepth: false', function (st) { + st.doesNotThrow( + function () { + var result = qs.parse('a[b]=c', { depth: 1 }); + st.deepEqual(result, { a: { b: 'c' } }, 'Should parse correctly'); + } + ); + st.end(); + }); + + t.test('does not throw when depth is exactly at the limit with strictDepth: true', function (st) { + st.doesNotThrow( + function () { + var result = qs.parse('a[b][c]=d', { depth: 2, strictDepth: true }); + st.deepEqual(result, { a: { b: { c: 'd' } } }, 'Should parse correctly'); + } + ); + st.end(); + }); +});