Skip to content

Commit

Permalink
feat: Protect against prototype poisoning
Browse files Browse the repository at this point in the history
follow fastify/fastify#1427

throw SyntaxError when prototype poisoning happen by default
  • Loading branch information
fengmk2 committed Jun 5, 2024
1 parent 9cce60c commit 3db8dd0
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 2 deletions.
4 changes: 4 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions lib/json.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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 });
}
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"urlencoded"
],
"dependencies": {
"@hapi/bourne": "^3.0.0",
"inflation": "^2.0.0",
"qs": "^6.5.2",
"raw-body": "^2.3.3",
Expand Down
54 changes: 54 additions & 0 deletions test/json.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});

0 comments on commit 3db8dd0

Please sign in to comment.