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();