BuilderIO / BuilderIO/builder

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

Open
#4,823 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
8.8k
Forks
1.2k
Avg merge
1d 6h
Merged PRs (30d)
17

Description

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](http://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:

```ts
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:

```ts
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.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in packages/sdks/src/functions/get-processed-block.ts and trace its calls into packages/sdks/src/functions/set.ts. Compare the existing deny-list in packages/core/src/classes/query-string.class.ts, then verify the SDK path handling for the listed dangerous segments. Done means a malicious bindings path no longer changes Object.prototype or affects later unrelated renders.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.