Secure deserialization in TypeScript means accepting plain data formats like JSON, validating the shape at runtime, never letting incoming data choose a type, and defending against prototype pollution when merging parsed objects. TypeScript types do not exist at runtime, so a cast is not validation.
Why is deserialization risky at all?
OWASP explains the root issue: native deserialization mechanisms often offer more features than JSON or XML, including customization, and those features can be repurposed against untrusted data. Attacks against deserializers have enabled denial of service, access control bypass and remote code execution. Its cross-language advice is to prefer a safe, standard data format such as JSON, and in .NET to not let the data stream define the type of object it becomes.
JSON.parse is much safer than formats that revive arbitrary objects, because it only produces plain objects, arrays, strings, numbers, booleans and null. The risks in TypeScript apps come from what happens next.
Where does TypeScript go wrong?
Constructed example. An endpoint accepts a settings update:
const body = JSON.parse(raw) as Settings;
Object.assign(currentSettings, body);
The cast tells the compiler to trust the shape. It checks nothing. Two problems follow. If the body carries fields you did not intend, they flow into your settings. And if it carries a special key, merging can alter shared prototypes.
What is prototype pollution?
The Node.js security guide describes it: attackers modify or inject properties into built-in language items by abusing proto, constructor, prototype and similar inherited properties. Its documented example parses JSON containing a proto key and merges it with Object.assign, which makes a property appear on later objects. A second example sets proto to null, causing a type error and denial of service when a method is later called.
The guide also gives realistic scenarios: a web API merges untrusted JSON request bodies into a shared configuration object; a template service passes user options into a deep merge utility, which then changes behavior for every later template.
Node's own threat model does not treat this as a core vulnerability because Node trusts the inputs that application code provides. The guide says you should defend at the application and dependency level.
What defenses does the guide list?
- Avoid insecure recursive merges.
- Validate external input with JSON Schema.
- Create objects without a prototype using Object.create(null).
- Freeze prototypes.
- Disable the proto property with the disable-proto flag.
- Check that a property exists on the object itself using Object.hasOwn.
- Avoid methods inherited from Object.prototype.
How do you validate at runtime?
Use a schema validator at the boundary. Define the schema once, derive the TypeScript type from it, and parse input through it so the compiler type and the runtime check cannot drift. Reject unknown keys, or strip them, depending on the endpoint. Set size limits on request bodies to reduce denial of service.
For formats beyond JSON, such as YAML, use the safe parsing mode of the library and avoid options that revive arbitrary types. Do not use eval or code-executing deserializers for data, ever.
What to do
- Use JSON for data exchange between services and clients.
- Replace every cast of parsed input with schema validation.
- Reject or strip unknown keys, and cap body size.
- Search for deep merge, extend and set-by-path utilities, and check how they treat proto and constructor keys.
- Build lookup tables with Object.create(null) or Map.
- Test with a payload containing a proto key and confirm nothing leaks.
- Never pass untrusted input to eval, the Function constructor or code-loading deserializers.
- Keep merge and parsing libraries patched, since prototype pollution flaws have recurred in them.
Safeguard's dependency scanning can flag vulnerable versions of merge and parsing libraries. The validation work is yours to write in code.
Quick check
Is JSON.parse itself vulnerable?
It only builds plain data, so it does not revive arbitrary objects. The risk comes from how you merge or use the result afterward, such as prototype pollution.
Does a TypeScript cast validate input?
No. Casts are erased at compile time and check nothing at runtime. Use a runtime schema validator.
Sources
Last reviewed: 18 October 2026.