diff --git a/README.md b/README.md index 4bb1f86..0a87c59 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,18 @@ console.log(deep); // } ``` -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): + +```js +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/src/parse.ts b/src/parse.ts index 3612d15..97372e2 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -23,6 +23,7 @@ const defaults = { parameterLimit: 1000, parseArrays: true, plainObjects: false, + strictDepth: false, strictNullHandling: false, } as NonNullableProperties; @@ -219,9 +220,14 @@ function parseKeys( 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) { + throw new RangeError( + 'Input depth exceeded depth option of ' + options.depth + ' and strictDepth is true', + ); + } keys.push('[' + key.slice(segment.index) + ']'); } @@ -307,6 +313,7 @@ function normalize_parse_options( 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 diff --git a/src/types.ts b/src/types.ts index eb32593..5ed7448 100644 --- a/src/types.ts +++ b/src/types.ts @@ -52,6 +52,7 @@ export type ParseBaseOptions = { allowPrototypes?: boolean; allowSparse?: boolean; parameterLimit?: number; + strictDepth?: boolean; strictNullHandling?: boolean; ignoreQueryPrefix?: boolean; charset?: 'utf-8' | 'iso-8859-1'; diff --git a/test/parse.test.ts b/test/parse.test.ts index 221006b..c4468e7 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -1267,3 +1267,64 @@ test('`duplicates` option', function (t) { // ); expect(parse('foo=bar&foo=baz', { duplicates: 'last' })).toEqual({ foo: 'baz' }); }); + +describe('qs strictDepth option - throw cases', function (t) { + test('throws an exception when depth exceeds the limit with strictDepth: true', function () { + expect(() => { + parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1, strictDepth: true }); + }).toThrowError(RangeError); + }); + + test('throws an exception for multiple nested arrays with strictDepth: true', function () { + expect(() => { + parse('a[0][1][2][3][4]=b', { depth: 3, strictDepth: true }); + }).toThrowError(RangeError); + }); + + test('throws an exception for nested objects and arrays with strictDepth: true', function (st) { + expect(() => { + parse('a[b][c][0][d][e]=f', { depth: 3, strictDepth: true }); + }).toThrowError(RangeError); + }); + + test('throws an exception for different types of values with strictDepth: true', function (st) { + expect(() => { + parse('a[b][c][d][e]=true&a[b][c][d][f]=42', { depth: 3, strictDepth: true }); + }).toThrowError(RangeError); + }); +}); + +describe('qs strictDepth option - non-throw cases', function (t) { + test('when depth is 0 and strictDepth true, do not throw', function (st) { + expect(() => { + parse('a[b][c][d][e]=true&a[b][c][d][f]=42', { depth: 0, strictDepth: true }); + }).not.toThrow(); + }); + + test('parses successfully when depth is within the limit with strictDepth: true', function (st) { + expect(() => { + parse('a[b]=c', { depth: 1, strictDepth: true }); + }).not.toThrow(); + }); + + test('does not throw an exception when depth exceeds the limit with strictDepth: false', function (st) { + expect(() => { + const result = parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1 }); + expect(result).toEqual({ a: { b: { '[c][d][e][f][g][h][i]': 'j' } } }); + }).not.toThrow(); + }); + + test('parses successfully when depth is within the limit with strictDepth: false', function (st) { + expect(() => { + const result = parse('a[b]=c', { depth: 1 }); + expect(result).toEqual({ a: { b: 'c' } }); + }).not.toThrow(); + }); + + test('does not throw when depth is exactly at the limit with strictDepth: true', function (st) { + expect(() => { + const result = parse('a[b][c]=d', { depth: 2, strictDepth: true }); + expect(result).toEqual({ a: { b: { c: 'd' } } }); + }).not.toThrow(); + }); +});