diff --git a/README.md b/README.md index 9fcd4c6f..b2ebf67f 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,16 @@ 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. +##### onProto + +###### onProtoPoisoning + +Defines what action must be taken when parsing a JSON object with `__proto__` + +###### onConstructorPoisoning + +Defines what action must be taken when parsing a JSON object with `constructor.prototype` key + ### bodyParser.raw([options]) Returns middleware that parses all bodies as a `Buffer` and only looks at diff --git a/lib/types/json.js b/lib/types/json.js index 078ce710..a8e4723c 100644 --- a/lib/types/json.js +++ b/lib/types/json.js @@ -18,6 +18,7 @@ var isFinished = require('on-finished').isFinished var read = require('../read') var typeis = require('type-is') var { getCharset, normalizeOptions } = require('../utils') +const secureJson = require('secure-json-parse') /** * Module exports. @@ -55,6 +56,7 @@ function json (options) { var reviver = options?.reviver var strict = options?.strict !== false + const poisoningOptions = { onProtoPoisoning: options?.onProto?.onProtoPoisoning || 'ignore', onConstructorPoisoning: options?.onProto?.onConstructorPoisoning || 'ignore' } function parse (body) { if (body.length === 0) { @@ -74,7 +76,7 @@ function json (options) { try { debug('parse json') - return JSON.parse(body, reviver) + return secureJson.parse(body, reviver, { protoAction: poisoningOptions.onProtoPoisoning, constructorAction: poisoningOptions.onConstructorPoisoning }) } catch (e) { throw normalizeJsonSyntaxError(e, { message: e.message, diff --git a/package.json b/package.json index b5dc8303..9f75bd8c 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", + "secure-json-parse": "^3.0.2", "type-is": "^2.0.1" }, "devDependencies": { diff --git a/test/json.js b/test/json.js index e679ac57..2a760f5a 100644 --- a/test/json.js +++ b/test/json.js @@ -721,6 +721,58 @@ describe('bodyParser.json()', function () { test.expect(413, done) }) }) + + describe('prototype poisoning', function () { + it('should parse __proto__ when protoAction is set to ignore', function (done) { + request(createServer({ onProto: { onProtoPoisoning: 'ignore' } })) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi","__proto__":{"x":7}}') + .expect(200, '{"user":"tobi","__proto__":{"x":7}}', done) + }) + + it('should throw when protoAction is set to error', function (done) { + request(createServer({ onProto: { onProtoPoisoning: 'error' } })) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi","__proto__":{"x":7}}') + .expect(400, '[entity.parse.failed] Object contains forbidden prototype property', done) + }) + + it('should remove prototype poisoning when protoAction is set to remove', function (done) { + request(createServer({ onProto: { onProtoPoisoning: 'remove' } })) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi","__proto__":{"x":7}}') + .expect(200, '{"user":"tobi"}', done) + }) + }) + + describe('constructor poisoning', function () { + it('should parse constructor when protoAction is set to ignore', function (done) { + request(createServer({ onProto: { onConstructorPoisoning: 'ignore' } })) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi","constructor":{"prototype":{"bar":"baz"}}}') + .expect(200, '{"user":"tobi","constructor":{"prototype":{"bar":"baz"}}}', done) + }) + + it('should throw when protoAction is set to error', function (done) { + request(createServer({ onProto: { onConstructorPoisoning: 'error' } })) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi","constructor":{"prototype":{"bar":"baz"}}}') + .expect(400, '[entity.parse.failed] Object contains forbidden prototype property', done) + }) + + it('should remove prototype poisoning when protoAction is set to remove', function (done) { + request(createServer({ onProto: { onConstructorPoisoning: 'remove' } })) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi","constructor":{"prototype":{"bar":"baz"}}}') + .expect(200, '{"user":"tobi"}', done) + }) + }) }) function createServer (opts) {