Skip to content

Commit

Permalink
feat: Optionally omit object key __proto__ and others from parsed output
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
prantlf committed Aug 9, 2024
1 parent 4b46756 commit e2f8a7b
Show file tree
Hide file tree
Showing 7 changed files with 59 additions and 8 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ Usage: `jsonlint [options] [--] [<file, directory, pattern> ...]`

-f, --config <file> 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 <id> locale identifier to sort object keys with
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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) |

Expand Down
14 changes: 13 additions & 1 deletion lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Usage: jsonlint [options] [--] [<file, directory, pattern> ...]
Options:
-f, --config <file> 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 <id> locale identifier to sort object keys with
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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) => {
Expand Down
4 changes: 3 additions & 1 deletion lib/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion src/configurable-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
16 changes: 12 additions & 4 deletions src/custom-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -313,8 +317,7 @@ function parseInternal (input, options) {
}

function parseObject () {
const result = {}
const emptyObject = {}
let result = {}
let isNotEmpty = false

while (position < inputLength) {
Expand Down Expand Up @@ -346,15 +349,20 @@ 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) {
value = reviver(key, value)
}
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
}
}
}

Expand Down
12 changes: 12 additions & 0 deletions test/parse2.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('')
Expand Down
12 changes: 11 additions & 1 deletion web/jsonlint.html
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ <h1>JSON Lint</h1>
<input type="checkbox" checked id="duplicate-object-keys">
<label for="duplicate-object-keys">Allow duplicate object keys</label>
</div>
<div>
<input type="checkbox" checked id="ignore-proto-key">
<label for="ignore-proto-key">Ignore __proto__ key</label>
</div>
<div>
<input type="checkbox" checked id="ignore-prototype-keys">
<label for="ignore-prototype-keys">Ignore Object.prototype keys</label>
</div>
</div>
<div>
<span>Formatting:</span>
Expand Down Expand Up @@ -203,7 +211,7 @@ <h2>Result</h2>
</main>
<hr>
<footer>
<small>Copyright &copy; 2012-2023 Zachary Carter, Ferdinand Prantl. See the <a href="https://github.com/prantlf/jsonlint#json-lint">project pages</a> to learn about command-line validation and programmatic usage.</small>
<small>Copyright &copy; 2012-2024 Zachary Carter, Ferdinand Prantl. See the <a href="https://github.com/prantlf/jsonlint#json-lint">project pages</a> to learn about command-line validation and programmatic usage.</small>
<!-- See http://tholman.com/github-corners/ -->
<a href="http://github.com/prantlf/jsonlint" class="github-corner" title="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style>
</footer>
Expand Down Expand Up @@ -367,6 +375,8 @@ <h2>Result</h2>
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
}
}
}
Expand Down

0 comments on commit e2f8a7b

Please sign in to comment.