Skip to content

Commit 5042698

Browse files
committed
fix(rest): sanitize json for JSON.parse()
1 parent 75731f9 commit 5042698

File tree

6 files changed

+106
-5
lines changed

6 files changed

+106
-5
lines changed

packages/rest/src/__tests__/unit/body-parser.unit.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,24 @@ describe('body parser', () => {
186186
});
187187
});
188188

189+
it('reports error for json payload with "__proto__" key', () => {
190+
const req = givenRequest({
191+
url: '/',
192+
payload: '{"x": 1, "__proto__": {"y": "2"}}',
193+
headers: {
194+
'Content-Type': 'application/json',
195+
},
196+
});
197+
198+
const spec = givenOperationWithRequestBody({
199+
description: 'data',
200+
content: {},
201+
});
202+
return expect(
203+
requestBodyParser.loadRequestBodyIfNeeded(spec, req),
204+
).to.be.rejectedWith('JSON string cannot contain "__proto__" key.');
205+
});
206+
189207
it('sorts body parsers', () => {
190208
const options: RequestBodyParserOptions = {};
191209
const bodyParser = new RequestBodyParser([
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright IBM Corp. 2018. All Rights Reserved.
2+
// Node module: @loopback/rest
3+
// This file is licensed under the MIT License.
4+
// License text available at https://opensource.org/licenses/MIT
5+
6+
import {expect} from '@loopback/testlab';
7+
import {parseJson} from '../../parse-json';
8+
9+
describe('parseJson', () => {
10+
it('throws for JSON text with __proto__ key', () => {
11+
const text = '{"x": "1", "__proto__": {"y": 2}}';
12+
expect(() => parseJson(text)).to.throw(
13+
'JSON string cannot contain "__proto__" key.',
14+
);
15+
});
16+
17+
it('throws for JSON text with deep __proto__ key', () => {
18+
const text = '{"x": "1", "y": {"__proto__": {"z": 2}}}';
19+
expect(() => parseJson(text)).to.throw(
20+
'JSON string cannot contain "__proto__" key.',
21+
);
22+
});
23+
24+
it('works for JSON text with deep __proto__ value', () => {
25+
const text = '{"x": "1", "y": "__proto__"}';
26+
expect(parseJson(text)).to.eql(JSON.parse(text));
27+
});
28+
29+
it('supports reviver function', () => {
30+
const text = '{"x": 1, "y": "2"}';
31+
const obj = parseJson(text, (key, value) => {
32+
if (key === 'y') return parseInt(value);
33+
return value;
34+
});
35+
expect(obj).to.eql({x: 1, y: 2});
36+
});
37+
});

packages/rest/src/body-parsers/body-parser.json.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
builtinParsers,
1616
} from './body-parser.helpers';
1717
import {BodyParser, RequestBody} from './types';
18+
import {sanitizeJsonParse} from '../parse-json';
1819

1920
export class JsonBodyParser implements BodyParser {
2021
name = builtinParsers.json;
@@ -25,6 +26,7 @@ export class JsonBodyParser implements BodyParser {
2526
options: RequestBodyParserOptions = {},
2627
) {
2728
const jsonOptions = getParserOptions('json', options);
29+
jsonOptions.reviver = sanitizeJsonParse(jsonOptions.reviver);
2830
this.jsonParser = json(jsonOptions);
2931
}
3032

packages/rest/src/coercion/coerce-parameter.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,21 @@
33
// This file is licensed under the MIT License.
44
// License text available at https://opensource.org/licenses/MIT
55

6-
import {ParameterObject, isReferenceObject} from '@loopback/openapi-v3-types';
7-
import {Validator} from './validator';
6+
import {isReferenceObject, ParameterObject} from '@loopback/openapi-v3-types';
87
import * as debugModule from 'debug';
98
import {RestHttpErrors} from '../';
9+
import {parseJson} from '../parse-json';
1010
import {
11+
DateCoercionOptions,
1112
getOAIPrimitiveType,
13+
IntegerCoercionOptions,
1214
isEmpty,
1315
isFalse,
1416
isTrue,
1517
isValidDateTime,
1618
matchDateFormat,
17-
DateCoercionOptions,
18-
IntegerCoercionOptions,
1919
} from './utils';
20+
import {Validator} from './validator';
2021
const isRFC3339 = require('validator/lib/isRFC3339');
2122
const debug = debugModule('loopback:rest:coercion');
2223

@@ -185,7 +186,7 @@ function parseJsonIfNeeded(
185186
}
186187

187188
try {
188-
const result = JSON.parse(data);
189+
const result = parseJson(data);
189190
debug('Parsed parameter %s as %j', spec.name, result);
190191
return result;
191192
} catch (err) {

packages/rest/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export * from './rest.component';
1919
export * from './rest.server';
2020
export * from './sequence';
2121
export * from './rest-http-error';
22+
export * from './parse-json';
2223

2324
// export all errors from external http-errors package
2425
import * as HttpErrors from 'http-errors';

packages/rest/src/parse-json.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright IBM Corp. 2018. All Rights Reserved.
2+
// Node module: @loopback/rest
3+
// This file is licensed under the MIT License.
4+
// License text available at https://opensource.org/licenses/MIT
5+
6+
//tslint:disable:no-any
7+
8+
// These utilities are introduced to mitigate the prototype pollution issue
9+
// with `JSON.parse`.
10+
// See https://hueniverse.com/a-tale-of-prototype-poisoning-2610fa170061
11+
//
12+
// The [bourne](https://github.com/hapijs/bourne) module provides a drop-in
13+
// replacement for `JSON.parse` but we need to instruct `body-parser` to honor
14+
// a `reviver` function.
15+
16+
/**
17+
* Factory to create a reviver function for `JSON.parse` to sanitize keys
18+
* @param reviver Reviver function
19+
*/
20+
export function sanitizeJsonParse(reviver?: (key: any, value: any) => any) {
21+
return (key: string, value: any) => {
22+
if (key === '__proto__')
23+
throw new Error('JSON string cannot contain "__proto__" key.');
24+
if (reviver) {
25+
return reviver(key, value);
26+
} else {
27+
return value;
28+
}
29+
};
30+
}
31+
32+
/**
33+
*
34+
* @param text JSON string
35+
* @param reviver Optional reviver function for `JSON.parse`
36+
*/
37+
export function parseJson(
38+
text: string,
39+
reviver?: (key: any, value: any) => any,
40+
) {
41+
return JSON.parse(text, sanitizeJsonParse(reviver));
42+
}

0 commit comments

Comments
 (0)