Affected Versions
| Affected | not applicable (first-party code) |
| Fixed in | commit-based fix (see PR) |
| Ecosystem | N/A |
| CVE / GHSA | not assigned |
| CWE | CWE-502 (Deserialization of Untrusted Data) |
Introduction
The parseToolCallInput function sits at a trust boundary most developers rarely examine: where your application receives structured data from an external API and transforms it into executable instructions. In the Anthropic API adapter, this function receives tool call arguments as JSON strings from the Codex backend and parses them into JavaScript objects. The vulnerability wasn't in using JSON.parse—it was in using it bare, without accounting for what happens when an attacker controls the string being parsed.
This is a defense-in-depth fix. No exploit has been demonstrated in the wild. But the pattern it closes—trusting API responses to contain only benign property names—is exactly where supply-chain and infrastructure attacks increasingly target.
The Vulnerability Explained
The vulnerable code in parseToolCallInput was straightforward:
function parseToolCallInput(argumentsJson: string): object {
// ... validation ...
try {
const parsed = JSON.parse(argumentsJson);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed;
}
}
// ...
}
The problematic line: JSON.parse(argumentsJson) without a reviver function.
JavaScript's JSON.parse creates objects with the default prototype chain. When parsing {"__proto__": {"polluted": true}}, the __proto__ key becomes a property assignment that, in many contexts, can overwrite Object.prototype. If parseToolCallInput returns this object and subsequent code merges it elsewhere (via Object.assign, spread syntax, or recursive merging), the pollution spreads.
Attack scenario: An attacker with MITM position or compromised Codex backend returns a tool call with arguments like:
{"__proto__": {"toString": "polluted", "isAdmin": true}}
If the extension later checks user.isAdmin on any object without that property, the polluted prototype chain returns true. The impact depends entirely on what checks exist downstream—logging bypasses, authorization flaws, or unexpected type coercion.
The parseToolCallInput function specifically handles tool call arguments, meaning this pollution vector could affect any extension using the Anthropic adapter to execute tool calls based on model-generated responses.
The Fix
The fix adds a reviver callback to JSON.parse that explicitly neutralizes the three dangerous keys:
const parsed = JSON.parse(argumentsJson, (key, value) => {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
return undefined;
}
return value;
});
Before: JSON.parse(argumentsJson) — accepts all keys, including prototype pollution vectors.
After: The reviver intercepts every key-value pair during parsing. For __proto__, constructor, or prototype, it returns undefined, effectively stripping these properties from the result. All other keys pass through unchanged.
This change is surgical. It doesn't alter the function's signature or return type. It doesn't add dependencies. It simply bounds the deserialization to safe property names, making the failure mode explicit: if an attacker tries to pollute the prototype, those properties vanish rather than propagate.
Key Takeaways
-
The
reviverparameter inJSON.parseis your last line of defense when parsing untrusted JSON. Most developers ignore it; security-critical code should use it. -
Tool call arguments from LLM APIs are untrusted data regardless of TLS. Backend compromise, DNS hijacking, or supply-chain attacks on the API provider can all inject malicious payloads.
-
__proto__,constructor, andprototypeare not valid data keys in any reasonable JSON schema. Rejecting them at parse time has no legitimate downside. -
Defense-in-depth at deserialization points prevents cascading failures. Even if upstream validation fails, the parser itself enforces safety.
-
TypeScript's
objectreturn type provides no protection against prototype pollution. The parsed result passes type checking while still being dangerous.
How Orbis AppSec Detected This
Source: The argumentsJson parameter to parseToolCallInput, originating from HTTP responses to the Anthropic/Codex API.
Sink: JSON.parse() invoked without a reviver function to filter dangerous keys.
Missing control: No validation that parsed object keys exclude __proto__, constructor, or prototype before returning the object to callers.
CWE: CWE-502 (Deserialization of Untrusted Data)
Fix: Added a reviver callback returning undefined for the three prototype pollution keys, explicitly bounding the deserialization failure mode.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
The parseToolCallInput fix demonstrates that deserialization hardening doesn't require massive refactoring. A nine-line change—adding a reviver callback with three specific key checks—eliminates a class of prototype pollution attacks against this code path. For developers integrating LLM APIs, this pattern should be standard: every JSON.parse of untrusted data deserves scrutiny of what keys it might create, and whether those keys could poison the objects everyone else relies on.