Skip to content

fix(ai,mcp): use prototype-safe writes for dynamic-key lookup maps - #20005

Open
trmxvibs wants to merge 4 commits into
vercel:mainfrom
trmxvibs:fix/prototype-pollution-adjacent-maps
Open

fix(ai,mcp): use prototype-safe writes for dynamic-key lookup maps#20005
trmxvibs wants to merge 4 commits into
vercel:mainfrom
trmxvibs:fix/prototype-pollution-adjacent-maps

Conversation

@trmxvibs

@trmxvibs trmxvibs commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes #18187 (finding 1: prototype-pollution-adjacent merge pattern)

Background

While auditing packages/ai and packages/mcp, five call sites were found building lookup objects keyed by external/dynamic strings using plain bracket assignment on a {}-literal (obj[dynamicKey] = value), instead of the codebase's own established prototype-safe helpers (getOwn, createIdMapObject.create(null)). A key of __proto__ at any of these sites would mutate the object's prototype instead of being stored as an own property.

Summary

  • packages/mcp/src/tool/mcp-client.ts and packages/ai/src/registry/provider-registry.ts: initialize the lookup map with Object.create(null), matching the existing createIdMap convention used elsewhere in the codebase.
  • packages/ai/src/embed/embed-many.ts, packages/ai/src/generate-image/generate-image.ts, packages/ai/src/generate-video/generate-video.ts: these accumulate providerMetadata that is returned to callers, and existing tests assert strict shape equality (toStrictEqual) against plain objects — changing the accumulator's own prototype broke those tests. Added a new setOwn() helper (packages/ai/src/util/set-own.ts, symmetric with the existing getOwn()) that uses Object.defineProperty to always write an own property regardless of key, without touching the target's prototype.

Contributor Credit

Reported and fixed by @trmxvibs (issue #18187, finding 1).

End-to-End Verification

  • Added packages/ai/src/util/set-own.test.ts covering __proto__, constructor, toString, valueOf, hasOwnProperty keys.
  • All existing tests in the touched files pass unchanged (204 tests across embed-many, generate-image, generate-video, start-video, provider-registry, plus the new set-own tests).
  • tsc --noEmit clean on both packages/ai and packages/mcp.
  • Manually verified with a standalone repro matching the original issue's PoC: Object.getPrototypeOf(obj) no longer resolves to an attacker-controlled object after assigning a __proto__ key through the fixed code paths.

Checklist

  • Tests have been added / updated (for bug fixes / features)
  • A patch changeset for relevant packages has been added
  • I have reviewed this pull request (self-review)

Future Work

Findings 2–4 from #18187 (canonicalJSON collision, missing chatId encoding, onError bypass for providerExecuted tool errors) are tracked separately and already have PRs open (#18394, #18395, #20001).

Related Issues

Fixes #18187

Five call sites built lookup objects keyed by external/dynamic strings (MCP tool names, provider names, provider ids) using plain bracket assignment on a {}-literal, which lets a key of "__proto__" mutate the object's prototype instead of being stored as an own property.

- packages/mcp/src/tool/mcp-client.ts and packages/ai/src/registry/provider-registry.ts now initialize their maps with Object.create(null), matching the codebase's existing createIdMap convention.

- embed-many.ts, generate-image.ts, and generate-video.ts accumulate providerMetadata that is returned to callers, and existing tests assert strict shape equality against plain objects, so changing the accumulator's own prototype would be an observable behavior change. Added a new setOwn() helper (symmetric with the existing getOwn()) that uses Object.defineProperty to always write an own property regardless of key, without touching the target's prototype.

Fixes vercel#18187 (finding 1: prototype-pollution-adjacent merge pattern)
// plain bracket assignment here would let a key named `__proto__`
// mutate Object.prototype instead of being stored as an own
// property.
if (providerMetadata[providerName] == null) {

@vercel vercel Bot Aug 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The __proto__ prototype-pollution guard in generateImage reads through plain bracket access, so the setOwn initialization is skipped for a __proto__ provider key and the subsequent .images.push throws a TypeError.

Fix on Vercel

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant