diff --git a/HISTORY.md b/HISTORY.md index 204ac76e..ffb80f32 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,9 @@ unreleased -========== - * deps: qs@6.12.3 +=================== + + * deps: qs@6.13.0 + * add `depth` option to customize the depth level in the parser + * IMPORTANT: The default `depth` level for parsing URL-encoded data is now `32` (previously was `Infinity`) 1.20.2 / 2023-02-21 =================== diff --git a/README.md b/README.md index 1278138f..f6661b7d 100644 --- a/README.md +++ b/README.md @@ -278,6 +278,10 @@ The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)` where `buf` is a `Buffer` of the raw request body and `encoding` is the encoding of the request. The parsing can be aborted by throwing an error. +#### depth + +The `depth` option is used to configure the maximum depth of the `qs` library when `extended` is `true`. This allows you to limit the amount of keys that are parsed and can be useful to prevent certain types of abuse. Defaults to `32`. It is recommended to keep this value as low as possible. + ## Errors The middlewares provided by this module create errors using the @@ -374,6 +378,10 @@ as well as in the `encoding` property. The `status` property is set to `415`, the `type` property is set to `'encoding.unsupported'`, and the `encoding` property is set to the encoding that is unsupported. +### The input exceeded the depth + +This error occurs when using `bodyParser.urlencoded` with the `extended` property set to `true` and the input exceeds the configured `depth` option. The `status` property is set to `400`. It is recommended to review the `depth` option and evaluate if it requires a higher value. When the `depth` option is set to `32` (default value), the error will not be thrown. + ## Examples ### Express/Connect top-level generic diff --git a/lib/types/urlencoded.js b/lib/types/urlencoded.js index b2ca8f16..886a3ce2 100644 --- a/lib/types/urlencoded.js +++ b/lib/types/urlencoded.js @@ -55,6 +55,9 @@ function urlencoded (options) { : opts.limit var type = opts.type || 'application/x-www-form-urlencoded' var verify = opts.verify || false + var depth = typeof opts.depth !== 'number' + ? Number(opts.depth || 32) + : opts.depth if (verify !== false && typeof verify !== 'function') { throw new TypeError('option verify must be function') @@ -118,7 +121,8 @@ function urlencoded (options) { encoding: charset, inflate: inflate, limit: limit, - verify: verify + verify: verify, + depth: depth }) } } @@ -133,12 +137,20 @@ function extendedparser (options) { var parameterLimit = options.parameterLimit !== undefined ? options.parameterLimit : 1000 + + var depth = typeof options.depth !== 'number' + ? Number(options.depth || 32) + : options.depth var parse = parser('qs') if (isNaN(parameterLimit) || parameterLimit < 1) { throw new TypeError('option parameterLimit must be a positive number') } + if(isNaN(depth) || depth < 0) { + throw new TypeError('option depth must be a zero or a positive number') + } + if (isFinite(parameterLimit)) { parameterLimit = parameterLimit | 0 } @@ -156,12 +168,23 @@ function extendedparser (options) { var arrayLimit = Math.max(100, paramCount) debug('parse extended urlencoding') - return parse(body, { - allowPrototypes: true, - arrayLimit: arrayLimit, - depth: Infinity, - parameterLimit: parameterLimit - }) + try { + return parse(body, { + allowPrototypes: true, + arrayLimit: arrayLimit, + depth: depth, + strictDepth: true, + parameterLimit: parameterLimit + }) + } catch (err) { + if (err instanceof RangeError) { + throw createError(400, 'The input exceeded the depth', { + type: 'querystring.parse.rangeError' + }) + } else { + throw err + } + } } } diff --git a/package.json b/package.json index ffabc735..01445323 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.12.3", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" diff --git a/test/urlencoded.js b/test/urlencoded.js index 10b8c4d4..fa5a815c 100644 --- a/test/urlencoded.js +++ b/test/urlencoded.js @@ -195,7 +195,7 @@ describe('bodyParser.urlencoded()', function () { it('should parse deep object', function (done) { var str = 'foo' - for (var i = 0; i < 500; i++) { + for (var i = 0; i < 32; i++) { str += '[p]' } @@ -213,13 +213,91 @@ describe('bodyParser.urlencoded()', function () { var depth = 0 var ref = obj.foo while ((ref = ref.p)) { depth++ } - assert.strictEqual(depth, 500) + assert.strictEqual(depth, 32) }) .expect(200, done) }) }) }) + + describe('with depth option', function () { + describe('when custom value set', function () { + + it('should reject non possitive numbers', function () { + assert.throws(createServer.bind(null, { extended: true, depth: -1 }), + /TypeError: option depth must be a zero or a positive number/) + assert.throws(createServer.bind(null, { extended: true, depth: NaN }), + /TypeError: option depth must be a zero or a positive number/) + assert.throws(createServer.bind(null, { extended: true, depth: "beep" }), + /TypeError: option depth must be a zero or a positive number/) + }) + + + it('should parse up to the specified depth', function (done) { + this.server = createServer({ extended:true, depth: 10 }) + request(this.server) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('a[b][c][d]=value') + .expect(200, '{"a":{"b":{"c":{"d":"value"}}}}', done) + }) + + it('should not parse beyond the specified depth', function (done) { + this.server = createServer({ extended:true, depth: 1 }) + request(this.server) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('a[b][c][d][e]=value') + .expect(400, '[querystring.parse.rangeError] The input exceeded the depth', done) + }) + }) + + + describe('when default value', function () { + before(function () { + this.server = createServer({ }) + }) + + it('should parse deeply nested objects', function (done) { + var deepObject = 'a' + for (var i = 0; i < 32; i++) { + deepObject += '[p]' + } + deepObject += '=value' + + request(this.server) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(deepObject) + .expect(function (res) { + var obj = JSON.parse(res.text) + var depth = 0 + var ref = obj.a + while ((ref = ref.p)) { depth++ } + assert.strictEqual(depth, 32) + }) + .expect(200, done) + }) + + it('should not parse beyond the specified depth', function (done) { + var deepObject = 'a'; + for (var i = 0; i < 33; i++) { + deepObject += '[p]'; + } + deepObject += '=value'; + + request(this.server) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(deepObject) + .expect(400, '[querystring.parse.rangeError] The input exceeded the depth', done); + }); + + }) + + }) + describe('with inflate option', function () { describe('when false', function () { before(function () {