Skip to content

Commit 2bc63a5

Browse files
authored
feat: Protect against prototype poisoning (#87)
follow fastify/fastify#1427 throw SyntaxError when prototype poisoning happen by default closes #70
1 parent 9cce60c commit 2bc63a5

File tree

4 files changed

+63
-2
lines changed

4 files changed

+63
-2
lines changed

Readme.md

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ $ npm install co-body
2424

2525
- `limit` number or string representing the request size limit (1mb for json and 56kb for form-urlencoded)
2626
- `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).
27+
- `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).
28+
See [Prototype-Poisoning](https://fastify.dev/docs/latest/Guides/Prototype-Poisoning/) for more details about prototype poisoning attacks.
29+
Possible values are `'error'`, `'remove'` and `'ignore'`.
30+
Default to `'error'`, it will throw a `SyntaxError` when `Prototype-Poisoning` happen.
2731
- `queryString` an object of options when parsing query strings and form data. See [qs](https://github.com/hapijs/qs) for more information.
2832
- `returnRawBody` when set to `true`, the return value of `co-body` will be an object with two properties: `{ parsed: /* parsed value */, raw: /* raw body */}`.
2933
- `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.

lib/json.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
const raw = require('raw-body');
88
const inflate = require('inflation');
9+
const bourne = require('@hapi/bourne');
910
const utils = require('./utils');
1011

1112
// Allowed whitespace is defined in RFC 7159
@@ -35,6 +36,7 @@ module.exports = async function(req, opts) {
3536
opts.encoding = opts.encoding || 'utf8';
3637
opts.limit = opts.limit || '1mb';
3738
const strict = opts.strict !== false;
39+
const protoAction = opts.onProtoPoisoning || 'error';
3840

3941
const str = await raw(inflate(req), opts);
4042
try {
@@ -47,13 +49,13 @@ module.exports = async function(req, opts) {
4749
}
4850

4951
function parse(str) {
50-
if (!strict) return str ? JSON.parse(str) : str;
52+
if (!strict) return str ? bourne.parse(str, { protoAction }) : str;
5153
// strict mode always return object
5254
if (!str) return {};
5355
// strict JSON test
5456
if (!strictJSONReg.test(str)) {
5557
throw new SyntaxError('invalid JSON, only supports object and array');
5658
}
57-
return JSON.parse(str);
59+
return bourne.parse(str, { protoAction });
5860
}
5961
};

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"urlencoded"
1414
],
1515
"dependencies": {
16+
"@hapi/bourne": "^3.0.0",
1617
"inflation": "^2.0.0",
1718
"qs": "^6.5.2",
1819
"raw-body": "^2.3.3",

test/json.test.js

+54
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,58 @@ describe('parse.json(req, opts)', function() {
158158
.expect(200, done);
159159
});
160160
});
161+
162+
describe('with valid onProtoPoisoning', function() {
163+
it('should parse with onProtoPoisoning = "error" by default', function(done) {
164+
const app = koa();
165+
166+
app.use(function* () {
167+
try {
168+
yield parse.json(this);
169+
} catch (err) {
170+
err.should.be.an.instanceOf(SyntaxError);
171+
err.message.should.equal('Object contains forbidden prototype property');
172+
err.status.should.equal(400);
173+
err.body.should.equal('{ "__proto__": { "boom": "💣" } }');
174+
done();
175+
}
176+
});
177+
178+
request(app.callback())
179+
.post('/')
180+
.set('content-type', 'application/json')
181+
.send('{ "__proto__": { "boom": "💣" } }')
182+
.end(function() {});
183+
});
184+
185+
it('should parse with onProtoPoisoning = "ignore"', function(done) {
186+
const app = koa();
187+
188+
app.use(function* () {
189+
this.body = yield parse.json(this, { onProtoPoisoning: 'ignore' });
190+
});
191+
192+
request(app.callback())
193+
.post('/')
194+
.set('content-type', 'application/json')
195+
.send('{ "__proto__": { "boom": "💣" }, "hello": "world" }')
196+
.expect({ ['__proto__']: { boom: '💣' }, hello: 'world' })
197+
.expect(200, done);
198+
});
199+
200+
it('should parse with onProtoPoisoning = "remove"', function(done) {
201+
const app = koa();
202+
203+
app.use(function* () {
204+
this.body = yield parse.json(this, { onProtoPoisoning: 'remove' });
205+
});
206+
207+
request(app.callback())
208+
.post('/')
209+
.set('content-type', 'application/json')
210+
.send('{ "__proto__": { "boom": "💣" }, "hello": "world" }')
211+
.expect({ hello: 'world' })
212+
.expect(200, done);
213+
});
214+
});
161215
});

0 commit comments

Comments
 (0)