Server-side prototype pollution via content block `bindings` keys in Builder SDKs
- 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