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:
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:
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.
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.tsprocesses every content block'sbindingsmap like this:
bindingis the literal object key from the block'sbindingsmap in the content JSON returnedby the Content API, used unvalidated as the write path for
packages/sdks/src/functions/set.ts, ahand-rolled reimplementation of lodash's
_.setwith no__proto__/prototype/constructorguard. A content block whose
bindingsincludes a key such as"component.options.__proto__.isAdmin": "true"pollutesObject.prototypefor the whole runningprocess (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, callinggetProcessedBlock()with:Before:
({}).isAdmin === undefined. After callinggetProcessedBlock()once on that block:({}).isAdmin === true, and a brand-new, completely unrelated object created afterwards alsoreturns
truefor.isAdmin. We further confirmed this crosses request/tenant boundaries: afterrendering 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 exactlythis (
PROPERTY_NAME_DENY_LIST = ['__proto__', 'prototype', 'constructor']), so this looks like aguard 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 topackages/sdks/src/helpers/flatten.ts'sunflatten(), reported separately) before assigning intoa path segment, rejecting or skipping
__proto__,prototype, andconstructorsegments.