From e2f8a7be3cdb78bf696ba4251e2b3053f5714c22 Mon Sep 17 00:00:00 2001 From: Ferdinand Prantl Date: Sat, 10 Aug 2024 00:20:55 +0200 Subject: [PATCH] feat: Optionally omit object key __proto__ and others from parsed output Add arguments `ignore-proto-key` and `ignore-prototype-keys` and their corresponding options. If not set, all objects keys will be retained in the parsed output by default. BREAKING CHANGE: Object key `__proto__` and other keys from `Object.prototype` are included in the parsed object by default. Earlier, no keys from `Object.prototype` were included. The new behaviour is consistent with `JSON.parse`. If you need the old behaviour, add the argument `ignore-prototype-keys` to the command line, or set the option `ignorePrototypeKeys` to `true`, when calling the `parse` method. If you don't have under control, what will happens with the parsed object, you should consider setting `ignoreProtoKey` to `true`, when calling the `parse` method, to prevent prototype pollution. --- README.md | 6 ++++++ lib/cli.js | 14 +++++++++++++- lib/validator.js | 4 +++- src/configurable-parser.js | 3 ++- src/custom-parser.js | 16 ++++++++++++---- test/parse2.js | 12 ++++++++++++ web/jsonlint.html | 12 +++++++++++- 7 files changed, 59 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2534e1d..51f20ae 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,8 @@ Usage: `jsonlint [options] [--] [ ...]` -f, --config read options from a custom configuration file -F, --no-config disable searching for configuration files + --ignore-proto-key ignore occurrences of "__proto__" object key + --ignore-prototype-keys ignore all keys from "Object.prototype" -s, --sort-keys sort object keys (not when prettifying) --sort-keys-ignore-case sort object keys ignoring the letter case --sort-keys-locale locale identifier to sort object keys with @@ -199,6 +201,8 @@ The configuration is an object with the following properties, described above, w | Parameter | Alias | | --------- | ----- | | patterns | | +| ignore-proto-key | ignoreProtoKey | +| ignore-prototype-keys | ignorePrototypeKeys | | sort-keys | sortKeys | | sort-keys-ignore-case | sortKeysIgnoreCase | | sort-keys-locale | sortKeysLocale | @@ -274,6 +278,8 @@ The `parse` method offers more detailed [error information](#error-handling), th | `ignoreTrailingCommas` | ignores trailing commas in objects and arrays (boolean) | | `allowSingleQuotedStrings` | accepts strings delimited by single-quotes too (boolean) | | `allowDuplicateObjectKeys` | allows reporting duplicate object keys as an error (boolean) | +| `ignoreProtoKey` | ignore occurrences of the `__proto__` object key (boolean) | +| `ignorePrototypeKeys` | ignore all keys from `Object.prototype` (boolean) | | `mode` | sets multiple options according to the type of input data (string) | | `reviver` | converts object and array values (function) | diff --git a/lib/cli.js b/lib/cli.js index 0c10fe3..a41930f 100755 --- a/lib/cli.js +++ b/lib/cli.js @@ -17,6 +17,8 @@ Usage: jsonlint [options] [--] [ ...] Options: -f, --config read options from a custom configuration file -F, --no-config disable searching for configuration files + --ignore-proto-key ignore occurrences of "__proto__" object key + --ignore-prototype-keys ignore all keys from "Object.prototype" -s, --sort-keys sort object keys (not when prettifying) --sort-keys-ignore-case sort object keys ignoring the letter case --sort-keys-locale locale identifier to sort object keys with @@ -89,6 +91,12 @@ for (let i = 2, l = argv.length; i < l; ++i) { case 'F': params.config = false return + case 'ignore-proto-key': + params.ignoreProtoKey = flag + return + case 'ignore-prototype-keys': + params.ignorePrototypeKeys = flag + return case 's': case 'sort-keys': params.sortKeys = flag return @@ -238,6 +246,8 @@ for (let i = 2, l = argv.length; i < l; ++i) { } const paramNames = { + 'ignore-proto-key': 'ignoreProtoKey', + 'ignore-prototype-keys': 'ignorePrototypeKeys', 'trailing-commas': 'trailingCommas', 'single-quoted-strings': 'singleQuotedStrings', 'duplicate-keys': 'duplicateKeys', @@ -323,7 +333,9 @@ function processContents (source, file) { ignoreComments: params.comments, ignoreTrailingCommas: params.trailingCommas || params.trimTrailingCommas, allowSingleQuotedStrings: params.singleQuotedStrings, - allowDuplicateObjectKeys: params.duplicateKeys + allowDuplicateObjectKeys: params.duplicateKeys, + ignoreProtoKey: params.ignoreProtoKey, + ignorePrototypeKeys: params.ignorePrototypeKeys } if (params.validate.length) { const schemas = params.validate.map((file, index) => { diff --git a/lib/validator.js b/lib/validator.js index b7d793a..e227fd5 100644 --- a/lib/validator.js +++ b/lib/validator.js @@ -167,7 +167,9 @@ ignoreComments: options.ignoreComments, ignoreTrailingCommas: options.ignoreTrailingCommas, allowSingleQuotedStrings: options.allowSingleQuotedStrings, - allowDuplicateObjectKeys: options.allowDuplicateObjectKeys + allowDuplicateObjectKeys: options.allowDuplicateObjectKeys, + ignoreProtoKey: options.ignoreProtoKey, + ignorePrototypeKeys: options.ignorePrototypeKeys } const validate = compileSchema(ajv, schema, parseOptions) return function (data, input, options) { diff --git a/src/configurable-parser.js b/src/configurable-parser.js index a7faf08..ea54cee 100644 --- a/src/configurable-parser.js +++ b/src/configurable-parser.js @@ -6,7 +6,8 @@ const oldNode = typeof process !== 'undefined' && process.version.startsWith('v4 function needsCustomParser (options) { return options.ignoreBOM || options.ignoreComments || options.ignoreTrailingCommas || options.allowSingleQuotedStrings || options.allowDuplicateObjectKeys === false || - options.mode === 'cjson' || options.mode === 'json5' || isSafari || oldNode + options.ignoreProtoKey || options.ignorePrototypeKeys || options.mode === 'cjson' || + options.mode === 'json5' || isSafari || oldNode } function getReviver (options) { diff --git a/src/custom-parser.js b/src/custom-parser.js index c863007..4624fed 100644 --- a/src/custom-parser.js +++ b/src/custom-parser.js @@ -37,6 +37,8 @@ const unescapeMap = { const ownsProperty = Object.prototype.hasOwnProperty +const emptyObject = {} + function parseInternal (input, options) { if (typeof input !== 'string' || !(input instanceof String)) { input = String(input) @@ -46,6 +48,8 @@ function parseInternal (input, options) { const ignoreBOM = options.ignoreBOM const ignoreComments = options.ignoreComments || options.mode === 'cjson' || json5 const ignoreTrailingCommas = options.ignoreTrailingCommas || json5 + const ignoreProtoKey = options.ignoreProtoKey + const ignorePrototypeKeys = options.ignorePrototypeKeys const allowSingleQuotedStrings = options.allowSingleQuotedStrings || json5 const allowDuplicateObjectKeys = options.allowDuplicateObjectKeys const reviver = options.reviver @@ -313,8 +317,7 @@ function parseInternal (input, options) { } function parseObject () { - const result = {} - const emptyObject = {} + let result = {} let isNotEmpty = false while (position < inputLength) { @@ -346,7 +349,8 @@ function parseInternal (input, options) { } } - if (key in emptyObject || emptyObject[key] != null) { + if ((ignorePrototypeKeys && (key in emptyObject || emptyObject[key] != null)) || + (ignoreProtoKey && key === '__proto__')) { // silently ignore it } else { if (reviver) { @@ -354,7 +358,11 @@ function parseInternal (input, options) { } if (value !== undefined) { isNotEmpty = true - result[key] = value + if (key === '__proto__') { + result = Object.assign(JSON.parse(`{"__proto__":${JSON.stringify(value)}}`), result) + } else { + result[key] = value + } } } diff --git a/test/parse2.js b/test/parse2.js index 9107644..d6cd443 100644 --- a/test/parse2.js +++ b/test/parse2.js @@ -168,6 +168,18 @@ test('no prototype pollution', function () { assert.notDeepEqual(parsed, { polluted: true }) }) +test('forbid __proto__ key', function () { + const parsed = parse('{ "constructor": true, "__proto__": { "polluted": true } }', { ignoreProtoKey: true }) + assert.notDeepEqual(parsed, JSON.parse('{ "constructor": true, "__proto__": { "polluted": true } }')) + assert.strictEqual(parsed.constructor, true) +}) + +test('forbid prototype keys', function () { + const parsed = parse('{ "constructor": true, "__proto__": { "polluted": true } }', { ignorePrototypeKeys: true }) + assert.notDeepEqual(parsed, JSON.parse('{ "constructor": true, "__proto__": { "polluted": true } }')) + assert.strictEqual(typeof parsed.constructor, 'function') +}) + test('random numbers', function () { for (let i = 0; i < 100; ++i) { const str = '-01.e'.split('') diff --git a/web/jsonlint.html b/web/jsonlint.html index e912bc1..4872bff 100644 --- a/web/jsonlint.html +++ b/web/jsonlint.html @@ -119,6 +119,14 @@

JSON Lint

+
+ + +
+
+ + +
Formatting: @@ -203,7 +211,7 @@

Result


- Copyright © 2012-2023 Zachary Carter, Ferdinand Prantl. See the project pages to learn about command-line validation and programmatic usage. + Copyright © 2012-2024 Zachary Carter, Ferdinand Prantl. See the project pages to learn about command-line validation and programmatic usage.
@@ -367,6 +375,8 @@

Result

allowSingleQuotedStrings: document.getElementById('single-quoted-strings').checked || mode === 'json5', allowDuplicateObjectKeys: document.getElementById('duplicate-object-keys').checked, + ignoreProtoKey: document.getElementById('ignore-proto-key').checked, + ignorePrototypeKeys: document.getElementById('ignore-prototype-keys').checked } } }