Skip to content

Commit

Permalink
fix(rest): sanitize json for JSON.parse()
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Feb 6, 2019
1 parent 75731f9 commit d7481d4
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 5 deletions.
37 changes: 37 additions & 0 deletions packages/rest/src/__tests__/unit/json-parse.unit.ts
@@ -0,0 +1,37 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/rest
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {expect} from '@loopback/testlab';
import {parseJson} from '../../json-parse';

describe('parseJson', () => {
it('throws for JSON text with __proto__ key', () => {
const text = '{"x": "1", "__proto__": {"y": 2}}';
expect(() => parseJson(text)).to.throw(
'JSON string cannot contain "__proto__" key.',
);
});

it('throws for JSON text with deep __proto__ key', () => {
const text = '{"x": "1", "y": {"__proto__": {"z": 2}}}';
expect(() => parseJson(text)).to.throw(
'JSON string cannot contain "__proto__" key.',
);
});

it('works for JSON text with deep __proto__ value', () => {
const text = '{"x": "1", "y": "__proto__"}';
expect(parseJson(text)).to.eql(JSON.parse(text));
});

it('supports reviver function', () => {
const text = '{"x": 1, "y": "2"}';
const obj = parseJson(text, (key, value) => {
if (key === 'y') return parseInt(value);
return value;
});
expect(obj).to.eql({x: 1, y: 2});
});
});
2 changes: 2 additions & 0 deletions packages/rest/src/body-parsers/body-parser.json.ts
Expand Up @@ -15,6 +15,7 @@ import {
builtinParsers,
} from './body-parser.helpers';
import {BodyParser, RequestBody} from './types';
import {sanitizeJsonParse} from '../json-parse';

export class JsonBodyParser implements BodyParser {
name = builtinParsers.json;
Expand All @@ -25,6 +26,7 @@ export class JsonBodyParser implements BodyParser {
options: RequestBodyParserOptions = {},
) {
const jsonOptions = getParserOptions('json', options);
jsonOptions.reviver = sanitizeJsonParse(jsonOptions.reviver);
this.jsonParser = json(jsonOptions);
}

Expand Down
11 changes: 6 additions & 5 deletions packages/rest/src/coercion/coerce-parameter.ts
Expand Up @@ -3,20 +3,21 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {ParameterObject, isReferenceObject} from '@loopback/openapi-v3-types';
import {Validator} from './validator';
import {isReferenceObject, ParameterObject} from '@loopback/openapi-v3-types';
import * as debugModule from 'debug';
import {RestHttpErrors} from '../';
import {parseJson} from '../json-parse';
import {
DateCoercionOptions,
getOAIPrimitiveType,
IntegerCoercionOptions,
isEmpty,
isFalse,
isTrue,
isValidDateTime,
matchDateFormat,
DateCoercionOptions,
IntegerCoercionOptions,
} from './utils';
import {Validator} from './validator';
const isRFC3339 = require('validator/lib/isRFC3339');
const debug = debugModule('loopback:rest:coercion');

Expand Down Expand Up @@ -185,7 +186,7 @@ function parseJsonIfNeeded(
}

try {
const result = JSON.parse(data);
const result = parseJson(data);
debug('Parsed parameter %s as %j', spec.name, result);
return result;
} catch (err) {
Expand Down
1 change: 1 addition & 0 deletions packages/rest/src/index.ts
Expand Up @@ -19,6 +19,7 @@ export * from './rest.component';
export * from './rest.server';
export * from './sequence';
export * from './rest-http-error';
export * from './json-parse';

// export all errors from external http-errors package
import * as HttpErrors from 'http-errors';
Expand Down
34 changes: 34 additions & 0 deletions packages/rest/src/json-parse.ts
@@ -0,0 +1,34 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/rest
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

//tslint:disable:no-any

/**
* Factory to create a reviver function for `JSON.parse` to sanitize keys
* @param reviver Reviver function
*/
export function sanitizeJsonParse(reviver?: (key: any, value: any) => any) {
return (key: string, value: any) => {
if (key === '__proto__')
throw new Error('JSON string cannot contain "__proto__" key.');
if (reviver) {
return reviver(key, value);
} else {
return value;
}
};
}

/**
* See https://hueniverse.com/a-tale-of-prototype-poisoning-2610fa170061
* @param text JSON string
* @param reviver Optional reviver function for `JSON.parse`
*/
export function parseJson(
text: string,
reviver?: (key: any, value: any) => any,
) {
return JSON.parse(text, sanitizeJsonParse(reviver));
}

0 comments on commit d7481d4

Please sign in to comment.