diff --git a/Readme.md b/Readme.md index 1ef6793..7246552 100644 --- a/Readme.md +++ b/Readme.md @@ -24,6 +24,10 @@ $ npm install co-body - `limit` number or string representing the request size limit (1mb for json and 56kb for form-urlencoded) - `strict` when set to `true`, JSON parser will only accept arrays and objects; when `false` will accept anything `JSON.parse` accepts. Defaults to `true`. (also `strict` mode will always return object). + - `onProtoPoisoning` Defines what action the `co-body` lib must take when parsing a JSON object with `__proto__`. This functionality is provided by [bourne](https://github.com/hapijs/bourne). + See [Prototype-Poisoning](https://fastify.dev/docs/latest/Guides/Prototype-Poisoning/) for more details about prototype poisoning attacks. + Possible values are `'error'`, `'remove'` and `'ignore'`. + Default to `'error'`, it will throw a `SyntaxError` when `Prototype-Poisoning` happen. - `queryString` an object of options when parsing query strings and form data. See [qs](https://github.com/hapijs/qs) for more information. - `returnRawBody` when set to `true`, the return value of `co-body` will be an object with two properties: `{ parsed: /* parsed value */, raw: /* raw body */}`. - `jsonTypes` is used to determine what media type **co-body** will parse as **json**, this option is passed directly to the [type-is](https://github.com/jshttp/type-is) library. diff --git a/lib/json.js b/lib/json.js index 9ef3898..50a3033 100644 --- a/lib/json.js +++ b/lib/json.js @@ -6,6 +6,7 @@ const raw = require('raw-body'); const inflate = require('inflation'); +const bourne = require('@hapi/bourne'); const utils = require('./utils'); // Allowed whitespace is defined in RFC 7159 @@ -35,6 +36,7 @@ module.exports = async function(req, opts) { opts.encoding = opts.encoding || 'utf8'; opts.limit = opts.limit || '1mb'; const strict = opts.strict !== false; + const protoAction = opts.onProtoPoisoning || 'error'; const str = await raw(inflate(req), opts); try { @@ -47,13 +49,13 @@ module.exports = async function(req, opts) { } function parse(str) { - if (!strict) return str ? JSON.parse(str) : str; + if (!strict) return str ? bourne.parse(str, { protoAction }) : str; // strict mode always return object if (!str) return {}; // strict JSON test if (!strictJSONReg.test(str)) { throw new SyntaxError('invalid JSON, only supports object and array'); } - return JSON.parse(str); + return bourne.parse(str, { protoAction }); } }; diff --git a/package.json b/package.json index cd56bd4..88f421c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "urlencoded" ], "dependencies": { + "@hapi/bourne": "^3.0.0", "inflation": "^2.0.0", "qs": "^6.5.2", "raw-body": "^2.3.3", diff --git a/test/json.test.js b/test/json.test.js index fa9a537..aef4154 100644 --- a/test/json.test.js +++ b/test/json.test.js @@ -158,4 +158,58 @@ describe('parse.json(req, opts)', function() { .expect(200, done); }); }); + + describe('with valid onProtoPoisoning', function() { + it('should parse with onProtoPoisoning = "error" by default', function(done) { + const app = koa(); + + app.use(function* () { + try { + yield parse.json(this); + } catch (err) { + err.should.be.an.instanceOf(SyntaxError); + err.message.should.equal('Object contains forbidden prototype property'); + err.status.should.equal(400); + err.body.should.equal('{ "__proto__": { "boom": "💣" } }'); + done(); + } + }); + + request(app.callback()) + .post('/') + .set('content-type', 'application/json') + .send('{ "__proto__": { "boom": "💣" } }') + .end(function() {}); + }); + + it('should parse with onProtoPoisoning = "ignore"', function(done) { + const app = koa(); + + app.use(function* () { + this.body = yield parse.json(this, { onProtoPoisoning: 'ignore' }); + }); + + request(app.callback()) + .post('/') + .set('content-type', 'application/json') + .send('{ "__proto__": { "boom": "💣" }, "hello": "world" }') + .expect({ ['__proto__']: { boom: '💣' }, hello: 'world' }) + .expect(200, done); + }); + + it('should parse with onProtoPoisoning = "remove"', function(done) { + const app = koa(); + + app.use(function* () { + this.body = yield parse.json(this, { onProtoPoisoning: 'remove' }); + }); + + request(app.callback()) + .post('/') + .set('content-type', 'application/json') + .send('{ "__proto__": { "boom": "💣" }, "hello": "world" }') + .expect({ hello: 'world' }) + .expect(200, done); + }); + }); });