Security
JSON input security: validate shape, bound work and preserve numbers
A runnable allowlist example shows why parsing, validation and safe state updates are separate tasks.
Parsing establishes syntax. Your application still has to decide which fields and types it accepts, how much work they may cause, and whether it is safe to copy them into application state.
An input contract you can run
This example accepts a small profile with exactly name and age. The 4 KiB text limit and allowed age range belong to this demonstration; they are not universal security limits. Download profile-validation.mjs and run node profile-validation.mjs.
import assert from 'node:assert/strict';
export function readProfile(text) {
// Local example only; limit received and decompressed bytes at the HTTP layer too.
if (Buffer.byteLength(text, 'utf8') > 4096) throw new Error('Example input limit exceeded');
const data = JSON.parse(text);
if (!data || typeof data !== 'object' || Array.isArray(data)) throw new Error('Expected object');
if (Object.keys(data).some(key => !['name', 'age'].includes(key))) throw new Error('Unknown field');
if (typeof data.name !== 'string' || data.name.length < 1 || data.name.length > 80) throw new Error('Invalid name');
if (!Number.isInteger(data.age) || data.age < 0 || data.age > 150) throw new Error('Invalid age');
// Construct the permitted result instead of merging untrusted keys into application state.
return { name: data.name, age: data.age };
}
assert.deepEqual(readProfile('{"name":"Ana","age":28}'), { name: 'Ana', age: 28 });
assert.throws(() => readProfile('{"name":"Ana","age":28,"__proto__":{"admin":true}}'));
assert.throws(() => readProfile('{"name":"Ana","age":"28"}'));
assert.equal({}.admin, undefined);
console.log('3 validation checks passed; Object.prototype unchanged');
Expected output: 3 validation checks passed; Object.prototype unchanged. The valid profile is accepted, an unexpected __proto__ member is rejected, and a string age is rejected. The function constructs a fresh result using named properties instead of recursively merging unknown keys.
The example validates a small flat record. It is not a general schema validator, a safe deep merge implementation or an HTTP server. Check nested objects at every level when your actual contract includes them. Unknown fields can be rejected, stripped or deliberately retained; choose explicitly and never pass them blindly to privileged options.
Bound resources before and after parsing
A text-size check runs after the string exists. At a network boundary, also cap incoming bytes, decompressed bytes, processing time and concurrent requests. Do not rely only on Content-Length: it can be absent and does not describe expanded content.
Large strings, many properties, deeply nested values and expensive regular expressions create different costs. Schema limits help after parsing; they cannot undo memory already allocated by a parser. A timer on the same event loop cannot interrupt a synchronous parse or validator. Use a worker/process boundary when work must be terminable, and size limits before that work begins.
Do not assume that a library advertised as protection against prototype poisoning also enforces a depth limit. Check its documented options. secure-json-parse, for example, documents handling of prototype-related keys.
Numbers need a cross-language contract
const rounded = JSON.parse('{"id":9007199254740993}');
console.log(String(rounded.id)); // 9007199254740992
const exact = JSON.parse('{"id":"9007199254740993"}');
console.log(BigInt(exact.id).toString()); // 9007199254740993
This is JavaScript Number behavior, not a requirement that every language parse all numbers as doubles. Python’s standard decoder normally uses int for JSON integers. If IDs travel through JavaScript, use decimal strings and validate their syntax; do not convert an already-rounded Number to a string and expect to recover it.
Schemas, rendering and logs
Accept trusted schemas where possible; arbitrary schemas and remote references are another input boundary. Budget validation work and resolve approved references locally. A successful schema check does not authorize a URL fetch, HTML insertion or database operation. Use the appropriate URL restrictions, safe DOM text APIs and parameterized database queries separately.
Use synthetic inputs for tests in a local process. Keep tokens, personal fields and whole request bodies out of diagnostic logs. The schema guide has positive and negative examples; the formatter only checks JSON syntax.
References: Python JSON decoder, Node process isolation, and the JSON Schema object reference.
Related tools
Frequently asked questions
Does JSON.parse itself pollute Object.prototype?
A parsed __proto__ key is an own data property. Unsafe later merges or assignments can turn that input into prototype pollution.
Does every JSON parser round integers above 2^53?
No. JavaScript Number has that precision limit; Python normally parses JSON integers as arbitrary-precision int. Cross-language identifiers should have an explicit representation.