diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35a7acdc..2f5499ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,9 @@ name: ci - on: -- pull_request -- push + push: + branches: + - master + pull_request: jobs: test: @@ -30,6 +31,11 @@ jobs: - Node.js 15.x - Node.js 16.x - Node.js 17.x + - Node.js 18.x + - Node.js 19.x + - Node.js 20.x + - Node.js 21.x + - Node.js 22.x include: - name: Node.js 0.8 @@ -46,69 +52,86 @@ jobs: npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - name: io.js 1.x - node-version: "1.8" + node-version: "1" npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - name: io.js 2.x - node-version: "2.5" + node-version: "2" npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - name: io.js 3.x - node-version: "3.3" + node-version: "3" npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - name: Node.js 4.x - node-version: "4.9" + node-version: "4" npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 - name: Node.js 5.x - node-version: "5.12" + node-version: "5" npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 - name: Node.js 6.x - node-version: "6.17" + node-version: "6" npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6 - name: Node.js 7.x - node-version: "7.10" + node-version: "7" npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6 - name: Node.js 8.x - node-version: "8.17" - npm-i: mocha@7.2.0 + node-version: "8" + npm-i: mocha@7.2.0 nyc@14.1.1 - name: Node.js 9.x - node-version: "9.11" - npm-i: mocha@7.2.0 + node-version: "9" + npm-i: mocha@7.2.0 nyc@14.1.1 - name: Node.js 10.x - node-version: "10.24" + node-version: "10" npm-i: mocha@8.4.0 - name: Node.js 11.x - node-version: "11.15" + node-version: "11" npm-i: mocha@8.4.0 - name: Node.js 12.x - node-version: "12.22" + node-version: "12" + npm-i: mocha@9.2.2 - name: Node.js 13.x - node-version: "13.14" + node-version: "13" + npm-i: mocha@9.2.2 - name: Node.js 14.x - node-version: "14.19" + node-version: "14" - name: Node.js 15.x - node-version: "15.14" + node-version: "15" - name: Node.js 16.x - node-version: "16.14" + node-version: "16" - name: Node.js 17.x - node-version: "17.8" + node-version: "17" + + - name: Node.js 18.x + node-version: "18" + + - name: Node.js 19.x + node-version: "19" + + - name: Node.js 20.x + node-version: "20" + + - name: Node.js 21.x + node-version: "21" + + - name: Node.js 22.x + node-version: "22.4.1" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install Node.js ${{ matrix.node-version }} shell: bash -eo pipefail -l {0} @@ -125,7 +148,12 @@ jobs: dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" - name: Configure npm - run: npm config set shrinkwrap false + run: | + if [[ "$(npm config get package-lock)" == "true" ]]; then + npm config set package-lock false + else + npm config set shrinkwrap false + fi - name: Remove npm module(s) ${{ matrix.npm-rm }} run: npm rm --silent --save-dev ${{ matrix.npm-rm }} @@ -139,8 +167,8 @@ jobs: shell: bash run: | # eslint for linting - # - remove on Node.js < 10 - if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then + # - remove on Node.js < 12 + if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 12 ]]; then node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ grep -E '^eslint(-|$)' | \ sort -r | \ @@ -157,7 +185,7 @@ jobs: echo "node@$(node -v)" echo "npm@$(npm -v)" npm -s ls ||: - (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print "::set-output name=" $2 "::" $3 }' + (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print $2 "=" $3 }' >> "$GITHUB_OUTPUT" - name: Run tests shell: bash @@ -183,7 +211,7 @@ jobs: fi - name: Upload code coverage - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: steps.list_env.outputs.nyc != '' with: name: coverage @@ -194,14 +222,14 @@ jobs: needs: test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install lcov shell: bash run: sudo apt-get -y install lcov - name: Collect coverage reports - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: coverage path: ./coverage diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 00000000..39372a22 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,69 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '16 21 * * 1' + push: + branches: [ "master" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + + steps: + - name: "Checkout code" + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.2 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@2f93e4319b2f04a2efc38fa7f78bd681bc3f7b2f # v2.23.2 + with: + sarif_file: results.sarif diff --git a/HISTORY.md b/HISTORY.md index e114f6af..81d23e06 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,24 @@ +1.20.3 / 2024-09-10 +=================== + + * 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 +=================== + + * Fix strict json error message on Node.js 19+ + * deps: content-type@~1.0.5 + - perf: skip value escaping when unnecessary + * deps: raw-body@2.5.2 + +1.20.1 / 2022-10-06 +=================== + + * deps: qs@6.11.0 + * perf: remove unnecessary object clone + 1.20.0 / 2022-04-02 =================== diff --git a/README.md b/README.md index 1149aff5..f6661b7d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # body-parser -[![NPM Version][npm-image]][npm-url] -[![NPM Downloads][downloads-image]][downloads-url] -[![Build Status][github-actions-ci-image]][github-actions-ci-url] +[![NPM Version][npm-version-image]][npm-url] +[![NPM Downloads][npm-downloads-image]][npm-url] +[![Build Status][ci-image]][ci-url] [![Test Coverage][coveralls-image]][coveralls-url] +[![OpenSSF Scorecard Badge][ossf-scorecard-badge]][ossf-scorecard-visualizer] Node.js body parsing middleware. @@ -277,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 @@ -346,7 +351,7 @@ call `req.setEncoding` when using this module. The `status` property is set to This error will occur when the request is no longer readable when this middleware attempts to read it. This typically means something other than a middleware from -this module read the reqest body already and the middleware was also configured to +this module read the request body already and the middleware was also configured to read the same request. The `status` property is set to `500` and the `type` property is set to `'stream.not.readable'`. @@ -373,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 @@ -454,11 +463,14 @@ app.use(bodyParser.text({ type: 'text/html' })) [MIT](LICENSE) -[npm-image]: https://img.shields.io/npm/v/body-parser.svg -[npm-url]: https://npmjs.org/package/body-parser -[coveralls-image]: https://img.shields.io/coveralls/expressjs/body-parser/master.svg +[ci-image]: https://badgen.net/github/checks/expressjs/body-parser/master?label=ci +[ci-url]: https://github.com/expressjs/body-parser/actions/workflows/ci.yml +[coveralls-image]: https://badgen.net/coveralls/c/github/expressjs/body-parser/master [coveralls-url]: https://coveralls.io/r/expressjs/body-parser?branch=master -[downloads-image]: https://img.shields.io/npm/dm/body-parser.svg -[downloads-url]: https://npmjs.org/package/body-parser -[github-actions-ci-image]: https://img.shields.io/github/workflow/status/expressjs/body-parser/ci/master?label=ci -[github-actions-ci-url]: https://github.com/expressjs/body-parser/actions/workflows/ci.yml +[node-version-image]: https://badgen.net/npm/node/body-parser +[node-version-url]: https://nodejs.org/en/download +[npm-downloads-image]: https://badgen.net/npm/dm/body-parser +[npm-url]: https://npmjs.org/package/body-parser +[npm-version-image]: https://badgen.net/npm/v/body-parser +[ossf-scorecard-badge]: https://api.scorecard.dev/projects/github.com/expressjs/body-parser/badge +[ossf-scorecard-visualizer]: https://ossf.github.io/scorecard-visualizer/#/projects/github.com/expressjs/body-parser \ No newline at end of file diff --git a/index.js b/index.js index 93c3a1ff..bb24d739 100644 --- a/index.js +++ b/index.js @@ -91,16 +91,15 @@ Object.defineProperty(exports, 'urlencoded', { */ function bodyParser (options) { - var opts = {} - - // exclude type option - if (options) { - for (var prop in options) { - if (prop !== 'type') { - opts[prop] = options[prop] - } + // use default type for parsers + var opts = Object.create(options || null, { + type: { + configurable: true, + enumerable: true, + value: undefined, + writable: true } - } + }) var _urlencoded = exports.urlencoded(opts) var _json = exports.json(opts) diff --git a/lib/types/json.js b/lib/types/json.js index c2745be3..59f3f7e2 100644 --- a/lib/types/json.js +++ b/lib/types/json.js @@ -39,6 +39,9 @@ module.exports = json var FIRST_CHAR_REGEXP = /^[\x20\x09\x0a\x0d]*([^\x20\x09\x0a\x0d])/ // eslint-disable-line no-control-regex +var JSON_SYNTAX_CHAR = '#' +var JSON_SYNTAX_REGEXP = /#+/g + /** * Create a middleware to parse JSON bodies. * @@ -152,15 +155,23 @@ function json (options) { function createStrictSyntaxError (str, char) { var index = str.indexOf(char) - var partial = index !== -1 - ? str.substring(0, index) + '#' - : '' + var partial = '' + + if (index !== -1) { + partial = str.substring(0, index) + JSON_SYNTAX_CHAR + + for (var i = index + 1; i < str.length; i++) { + partial += JSON_SYNTAX_CHAR + } + } try { JSON.parse(partial); /* istanbul ignore next */ throw new SyntaxError('strict violation') } catch (e) { return normalizeJsonSyntaxError(e, { - message: e.message.replace('#', char), + message: e.message.replace(JSON_SYNTAX_REGEXP, function (placeholder) { + return str.substring(index, index + placeholder.length) + }), stack: e.stack }) } diff --git a/lib/types/urlencoded.js b/lib/types/urlencoded.js index b2ca8f16..2bd4485f 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 9a03357f..3c9926fc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "body-parser", "description": "Node.js body parsing middleware", - "version": "1.20.0", + "version": "1.20.3", "contributors": [ "Douglas Christopher Wilson ", "Jonathan Ong (http://jongleberry.com)" @@ -10,31 +10,31 @@ "repository": "expressjs/body-parser", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.10.3", - "raw-body": "2.5.1", + "qs": "6.13.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, "devDependencies": { - "eslint": "7.32.0", + "eslint": "8.34.0", "eslint-config-standard": "14.1.1", - "eslint-plugin-import": "2.25.4", - "eslint-plugin-markdown": "2.2.1", + "eslint-plugin-import": "2.27.5", + "eslint-plugin-markdown": "3.0.0", "eslint-plugin-node": "11.1.0", - "eslint-plugin-promise": "5.2.0", + "eslint-plugin-promise": "6.1.1", "eslint-plugin-standard": "4.1.0", "methods": "1.1.2", - "mocha": "9.2.2", + "mocha": "10.2.0", "nyc": "15.1.0", "safe-buffer": "5.2.1", - "supertest": "6.2.2" + "supertest": "6.3.3" }, "files": [ "lib/", diff --git a/test/body-parser.js b/test/body-parser.js index d46ea772..2d764d3a 100644 --- a/test/body-parser.js +++ b/test/body-parser.js @@ -73,13 +73,25 @@ describe('bodyParser()', function () { }) }) + function getMajorVersion (versionString) { + return versionString.split('.')[0] + } + + function shouldSkipQuery (versionString) { + // Skipping HTTP QUERY tests on Node 21, it is reported in http.METHODS on 21.7.2 but not supported + // update this implementation to run on supported versions of 21 once they exist + // upstream tracking https://github.com/nodejs/node/issues/51562 + // express tracking issue: https://github.com/expressjs/express/issues/5615 + return getMajorVersion(versionString) === '21' + } + methods.slice().sort().forEach(function (method) { - if (method === 'connect') { - // except CONNECT - return - } + if (method === 'connect') return it('should support ' + method.toUpperCase() + ' requests', function (done) { + if (method === 'query' && shouldSkipQuery(process.versions.node)) { + this.skip() + } request(this.server)[method]('/') .set('Content-Type', 'application/json') .set('Content-Length', '15') diff --git a/test/json.js b/test/json.js index 4d686763..c76ea138 100644 --- a/test/json.js +++ b/test/json.js @@ -245,7 +245,7 @@ describe('bodyParser.json()', function () { .post('/') .set('Content-Type', 'application/json') .send('true') - .expect(400, '[entity.parse.failed] ' + parseError('#rue').replace('#', 't'), done) + .expect(400, '[entity.parse.failed] ' + parseError('#rue').replace(/#/g, 't'), done) }) }) @@ -273,7 +273,7 @@ describe('bodyParser.json()', function () { .post('/') .set('Content-Type', 'application/json') .send('true') - .expect(400, '[entity.parse.failed] ' + parseError('#rue').replace('#', 't'), done) + .expect(400, '[entity.parse.failed] ' + parseError('#rue').replace(/#/g, 't'), done) }) it('should not parse primitives with leading whitespaces', function (done) { @@ -281,7 +281,7 @@ describe('bodyParser.json()', function () { .post('/') .set('Content-Type', 'application/json') .send(' true') - .expect(400, '[entity.parse.failed] ' + parseError(' #rue').replace('#', 't'), done) + .expect(400, '[entity.parse.failed] ' + parseError(' #rue').replace(/#/g, 't'), done) }) it('should allow leading whitespaces in JSON', function (done) { @@ -299,7 +299,7 @@ describe('bodyParser.json()', function () { .set('X-Error-Property', 'stack') .send('true') .expect(400) - .expect(shouldContainInBody(parseError('#rue').replace('#', 't'))) + .expect(shouldContainInBody(parseError('#rue').replace(/#/g, 't'))) .end(done) }) }) diff --git a/test/urlencoded.js b/test/urlencoded.js index 10b8c4d4..970c1e12 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,85 @@ 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 () { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy