1 | // Copyright IBM Corp. and LoopBack contributors 2019. 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 | /* eslint-disable @typescript-eslint/no-explicit-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 | const isMatched = (key: string, pattern: string) =>
|
17 | pattern === key || key.indexOf(`${pattern}.`) === 0;
|
18 |
|
19 | /**
|
20 | * Factory to create a reviver function for `JSON.parse` to sanitize keys
|
21 | * @param reviver - Reviver function
|
22 | * @param prohibitedKeys - An array of keys to be rejected
|
23 | */
|
24 | export function sanitizeJsonParse(
|
25 | reviver?: (key: any, value: any) => any,
|
26 | prohibitedKeys?: string[],
|
27 | ) {
|
28 | return (key: string, value: any) => {
|
29 | if (key === '__proto__') {
|
30 | // Reject `__proto__`
|
31 | throw new Error(`JSON string cannot contain "${key}" key.`);
|
32 | }
|
33 | if (
|
34 | key === 'constructor' &&
|
35 | value != null &&
|
36 | Object.keys(value).some(k => isMatched(k, 'prototype'))
|
37 | ) {
|
38 | // Reject `constructor/prototype.*`
|
39 | throw new Error(
|
40 | `JSON string cannot contain "constructor.prototype" key.`,
|
41 | );
|
42 | }
|
43 | if (prohibitedKeys?.some(pattern => isMatched(key, pattern))) {
|
44 | throw new Error(`JSON string cannot contain "${key}" key.`);
|
45 | }
|
46 | if (reviver) {
|
47 | return reviver(key, value);
|
48 | } else {
|
49 | return value;
|
50 | }
|
51 | };
|
52 | }
|
53 |
|
54 | /**
|
55 | * Parse a json string that rejects prohibited keys
|
56 | * @param text - JSON string
|
57 | * @param reviver - Optional reviver function for `JSON.parse`
|
58 | * @param prohibitedKeys - An array of keys to be rejected
|
59 | */
|
60 | export function parseJson(
|
61 | text: string,
|
62 | reviver?: (key: any, value: any) => any,
|
63 | prohibitedKeys?: string[],
|
64 | ) {
|
65 | prohibitedKeys = [
|
66 | '__proto__',
|
67 | 'constructor.prototype',
|
68 | ...(prohibitedKeys ?? []),
|
69 | ];
|
70 | return JSON.parse(text, sanitizeJsonParse(reviver, prohibitedKeys));
|
71 | }
|