#4823·builder

Server-side prototype pollution via content block `bindings` keys in Builder SDKs

Author: geo-chenCreated Aug 25, 2026Updated Aug 25, 2026

reported via email on 5 July 2026:

We found a prototype-pollution issue in the Gen2 Builder SDKs (packages/sdks, shared by @builder.io/sdk-react, sdk-vue, sdk-svelte, sdk-qwik, sdk-solid, sdk-react-nextjs, sdk-angular).

packages/sdks/src/functions/get-processed-block.ts processes every content block's bindings map like this:

typescript
for (const binding in block.bindings) {
  const expression = block.bindings[binding];
  const value = evaluate({ code: expression, localState, rootState, rootSetState, context });
  set(copied, binding, value);
}

binding is the literal object key from the block's bindings map in the content JSON returned by the Content API, used unvalidated as the write path for packages/sdks/src/functions/set.ts, a hand-rolled reimplementation of lodash's _.set with no __proto__/prototype/constructor guard. A content block whose bindings includes a key such as "component.options.__proto__.isAdmin": "true" pollutes Object.prototype for the whole running process (Node.js SSR and browser alike), affecting all subsequent objects created in that process (other requests, other tenants' renders on the same SSR instance) until it restarts.

We confirmed this live against the unmodified source at commit 27e443e125c8e7f619335403cc0ecb01e03197a0, calling getProcessedBlock() with:

typescript
const maliciousBuilderBlock = {
  '@type': '@[builder.io/sdk:Element](http://builder.io/sdk:Element)',
  id: 'builder-abc123',
  component: { name: 'Text', options: { text: 'hello world' } },
  bindings: { 'component.options.__proto__.isAdmin': 'true' },
};

Before: ({}).isAdmin === undefined. After calling getProcessedBlock() once on that block: ({}).isAdmin === true, and a brand-new, completely unrelated object created afterwards also returns true for .isAdmin. We further confirmed this crosses request/tenant boundaries: after rendering the malicious block once, a second, unrelated, benign block (no bindings at all) rendered afterwards in the same process comes back carrying the injected property too.

For comparison, the older Gen1 SDK's own query-string parser (packages/core/src/classes/query-string.class.ts) already has an explicit deny-list for exactly this (PROPERTY_NAME_DENY_LIST = ['__proto__', 'prototype', 'constructor']), so this looks like a guard that was never carried over to the newer SDKs' set()/unflatten() utilities.

Suggested fix: add the same deny-list check to packages/sdks/src/functions/set.ts (and to packages/sdks/src/helpers/flatten.ts's unflatten(), reported separately) before assigning into a path segment, rejecting or skipping __proto__, prototype, and constructor segments.