diff --git a/API.md b/API.md index c4c419118..cea622821 100755 --- a/API.md +++ b/API.md @@ -130,6 +130,7 @@ - [`route.options.payload.output`](#route.options.payload.output) - [`route.options.payload.override`](#route.options.payload.override) - [`route.options.payload.parse`](#route.options.payload.parse) + - [`route.options.payload.protoAction`](#route.options.payload.protoAction) - [`route.options.payload.timeout`](#route.options.payload.timeout) - [`route.options.payload.uploads`](#route.options.payload.uploads) - [`route.options.plugins`](#route.options.plugins) @@ -3258,6 +3259,20 @@ Determines if the incoming payload is processed or presented raw. Available valu - `'gunzip'` - the raw payload is returned unmodified after any known content encoding is decoded. +#### `route.options.payload.protoAction` + +Default value: `'error'`. + +Sets handling of incoming payload that may contain a prototype poisoning security attach. Available +values: + +- `'error'` - returns a `400` bad request error when the payload contains a prototype. + +- `'remove'` - sanitizes the payload to remove the prototype. + +- `'ignore'` - disables the protection and allows the payload to pass as received. Use this option + only when you are sure that such incoming data cannot pose any risks to your application. + #### `route.options.payload.timeout` Default value: to `10000` (10 seconds). diff --git a/lib/config.js b/lib/config.js index 1c9372a2e..0b63f8647 100755 --- a/lib/config.js +++ b/lib/config.js @@ -145,6 +145,7 @@ internals.routeBase = Joi.object({ .allow(false), allow: Joi.array().items(Joi.string()).single(), override: Joi.string(), + protoAction: Joi.valid('error', 'remove', 'ignore').default('error'), maxBytes: Joi.number().integer().positive().default(1024 * 1024), uploads: Joi.string().default(Os.tmpdir()), failAction: internals.failAction, diff --git a/test/payload.js b/test/payload.js index 71f83ce4f..e7ab4be52 100755 --- a/test/payload.js +++ b/test/payload.js @@ -125,6 +125,56 @@ describe('Payload', () => { expect(res.result.message).to.equal('Payload content length greater than maximum allowed: 10'); }); + it('errors when payload contains prototype poisoning', async () => { + + const server = Hapi.server(); + server.route({ method: 'POST', path: '/', handler: (request) => request.payload.x }); + + const payload = '{"x":"1","y":"2","z":"3","__proto__":{"x":"4"}}'; + const res = await server.inject({ method: 'POST', url: '/', payload }); + expect(res.statusCode).to.equal(400); + }); + + it('ignores when payload contains prototype poisoning', async () => { + + const server = Hapi.server(); + server.route({ + method: 'POST', + path: '/', + options: { + payload: { + protoAction: 'ignore' + }, + handler: (request) => request.payload.__proto__ + } + }); + + const payload = '{"x":"1","y":"2","z":"3","__proto__":{"x":"4"}}'; + const res = await server.inject({ method: 'POST', url: '/', payload }); + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal({ x: '4' }); + }); + + it('sanitizes when payload contains prototype poisoning', async () => { + + const server = Hapi.server(); + server.route({ + method: 'POST', + path: '/', + options: { + payload: { + protoAction: 'remove' + }, + handler: (request) => request.payload.__proto__ + } + }); + + const payload = '{"x":"1","y":"2","z":"3","__proto__":{"x":"4"}}'; + const res = await server.inject({ method: 'POST', url: '/', payload }); + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal({}); + }); + it('returns 413 with response when payload is not consumed', async () => { const payload = Buffer.alloc(10 * 1024 * 1024).toString();