Is Supademo: AI interactive demos in seconds safe?

Medium risk

Supademo records full DOM snapshots of pages you capture and sends debug logs with the active tab URL and account details to a third-party logger.

When you record an interactive demo, Supademo serializes the captured page's full DOM, including form field values (passwords and user-masked fields excluded), computed styles and canvas images, and uploads it to its media servers. Separately, its injected content scripts batch debug/info/warn logs and POST them every five seconds to the logging vendor BetterStack, attaching the recorded tab's URL plus the signed-in user's id, workspace id and name. Both behaviors occur only on tabs the user actively records.

Supademov7.6.0Chrome Web Store
45Risk
Who publishes it

Supademo, Inc. - no other listings under this identity

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
Supademo
Declared legal entity
Supademo, Inc.
Registered address
13331 Hauser Street, Overland Park, KS 66213, US
Registered contact
Joseph Lee

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

Recording Logs Send Tab URLs to BetterStack

MediumunwantedCWE-359Dynamic sandbox

We observed the extension post recording-log batches to BetterStack during an active Supademo recording.

What actually happens

You did this

You start recording or redacting a Supademo demo in the current tab.

That action causes the extension to run content scripts inside the page being recorded.

The extension did this

The content script queues log entries that include the current tab URL and account workspace context.

A timer posts the queued entries to BetterStack about every five seconds while entries are present.

When this fires

Every 5 seconds

Log entries are held in an in-memory queue and sent in batches about every five seconds when there is something to send.

Fields the logger adds to each BetterStack batch

  • Recorded tab URL
    https://app.example.com/admin/users?workspace=acme (illustrative)

    Shows the full page address open in the tab being recorded, which can expose the application, route, and query text you were viewing.

  • Signed-in account
    userId: user_2N7fK9aQ, name: Jane Doe (illustrative)

    Links the log entry to the Supademo account used during the recording session.

  • Workspace context
    workspaceId: ws_8c2fb1e4 (illustrative)

    Connects the recording activity to the workspace stored by the extension.

  • Browser tab list
    id: 123, isActive: true, windowId: 7 (illustrative)

    Adds browser context about open tabs, including which tab is active and the window it belongs to.

  • Runtime environment
    browser: Chrome, os: Mac, env: production, version: 7.4.8 (illustrative)

    Adds browser, operating system, environment, and extension version details to the same log entry.

Captured request

POSThttps://s1270069.eu-nbg-2.betterstackdata.com

Observed during dynamic analysis as a BetterStack log-ingestion POST; the available evidence confirms the JSON body contained context.auth and context.activeTab.url fields.

Headers
Content-Type
application/json
Authorization
Bearer <redacted>

The code that does this

The logger queues account and tab context, then posts the batch to BetterStack

Readable version

content.js deobfuscated logger and sender

content-scripts/content.js
async function Hz() {    if (Dl.length === 0) return;    const e = "https://s1270069.eu-nbg-2.betterstackdata.com",      t = "<redacted>",      n = JSON.stringify(Dl);    Nz();    try {      await xv(e, {        method: "POST",        headers: {          "Content-Type": "application/json",          Authorization: `Bearer ${t}`        },        body: n      })    } catch {}  }function Bz(e) {    e.addReporter({      log(t, n) {        const s = tn(t.type).with("trace", () => "debug").with("debug", () => "debug").with("info", () => "info").with("success", () => "info").with("warn", () => "warn").with("error", () => "warn").with("fatal", () => "error").otherwise(() => "debug"),          r = `${t.date.toISOString()}`,          o = t.args[1],          i = (o == null ? void 0 : o.slug) ?? null,          a = hg(o) ? sl(t.args[1], ["slug"]) : o,          l = t.args[0];        Promise.all([xi.getValue(), E1.getValue(), po.getValue(), Il.getValue(), Pr.getValue(), Iz(), Lz(), Vz()]).then(([u, c, d, h, f, w, y, v]) => {          Dl.push({            level: s,            tag: n.options.defaults.tag ?? "Unknown",            message: l,            slug: i,            dt: r,            extraData: a ?? void 0,            context: {              slug: i,              health: u,              auth: c,              settings: d,              demo: h,              activeTab: {                id: w.id,                url: w.url              },              tabs: y.map(b => ({                id: b.id ?? null,                isActive: b.active,                windowId: b.windowId              })),              redactor: f            },            environment: {              os: v,              browser: Pz(),              env: "production",              version: "7.4.8"            }          })        })      }    }), e.addReporter({      log(t, n) {        var i;        const s = tn(t.type).with("trace", () => "debug").with("debug", () => "debug").with("info", () => "debug").with("warn", () => "warning").with("error", () => "error").with("fatal", () => "fatal").otherwise(() => "debug"),          r = ((i = t.args[0]) == null ? void 0 : i.message) || t.args[0],          o = t.args[1];        if (s === "fatal") Promise.all([E1.getValue(), po.getValue(), Il.getValue(), Pr.getValue()]).then(([a, l, u, c]) => {          tr.setUser({            id: (a == null ? void 0 : a.userId) ?? "UNAUTHENTICATED",            username: (a == null ? void 0 : a.name) ?? "UNAUTHENTICATED"          }), tr.setTags({            demoId: u.id,            userId: (a == null ? void 0 : a.userId) ?? "UNAUTHENTICATED",            workspaceId: (a == null ? void 0 : a.workspaceId) ?? "UNAUTHENTICATED",            recordingType: u.type,            isAddingSteps: u.isAddingStepsInBetween,            isRedactorEnabled: c.mode !== "none"          }), tr.setContext("settings", {            ...l          }), tr.setContext("demo", {            ...u          }), tr.setContext("redactor", {            ...c          });          const d = new Error(o);          d.name = r, tr.captureException(d)        }).catch(a => {          console.error("Error while getting data for Sentry", a)        });        else {          const a = hg(o) ? o : {              extraData: o            },            l = sl(a, ["slug"]);          tr.addBreadcrumb({            data: l,            message: r,            level: s,            category: n.options.defaults.tag          })        }      }    })  }

redactor.js deobfuscated logger and sender

content-scripts/redactor.js
async function ph() {    if (zn.length === 0) return;    const e = "https://s1270069.eu-nbg-2.betterstackdata.com",      t = "<redacted>",      n = JSON.stringify(zn);    dh();    try {      await wa(e, {        method: "POST",        headers: {          "Content-Type": "application/json",          Authorization: `Bearer ${t}`        },        body: n      })    } catch {}  }function mh(e) {    e.addReporter({      log(t, n) {        const r = wn(t.type).with("trace", () => "debug").with("debug", () => "debug").with("info", () => "info").with("success", () => "info").with("warn", () => "warn").with("error", () => "warn").with("fatal", () => "error").otherwise(() => "debug"),          s = `${t.date.toISOString()}`,          o = t.args[1],          i = (o == null ? void 0 : o.slug) ?? null,          a = So(o) ? bn(t.args[1], ["slug"]) : o,          c = t.args[0];        Promise.all([oh.getValue(), Na.getValue(), ka.getValue(), $a.getValue(), en.getValue(), yh(), gh(), rh()]).then(([l, u, f, h, d, E, g, p]) => {          zn.push({            level: r,            tag: n.options.defaults.tag ?? "Unknown",            message: c,            slug: i,            dt: s,            extraData: a ?? void 0,            context: {              slug: i,              health: l,              auth: u,              settings: f,              demo: h,              activeTab: {                id: E.id,                url: E.url              },              tabs: g.map(m => ({                id: m.id ?? null,                isActive: m.active,                windowId: m.windowId              })),              redactor: d            },            environment: {              os: p,              browser: sh(),              env: "production",              version: "7.4.8"            }          })        })      }    }), e.addReporter({      log(t, n) {        var i;        const r = wn(t.type).with("trace", () => "debug").with("debug", () => "debug").with("info", () => "debug").with("warn", () => "warning").with("error", () => "error").with("fatal", () => "fatal").otherwise(() => "debug"),          s = ((i = t.args[0]) == null ? void 0 : i.message) || t.args[0],          o = t.args[1];        if (r === "fatal") Promise.all([Na.getValue(), ka.getValue(), $a.getValue(), en.getValue()]).then(([a, c, l, u]) => {          nt.setUser({            id: (a == null ? void 0 : a.userId) ?? "UNAUTHENTICATED",            username: (a == null ? void 0 : a.name) ?? "UNAUTHENTICATED"          }), nt.setTags({            demoId: l.id,            userId: (a == null ? void 0 : a.userId) ?? "UNAUTHENTICATED",            workspaceId: (a == null ? void 0 : a.workspaceId) ?? "UNAUTHENTICATED",            recordingType: l.type,            isAddingSteps: l.isAddingStepsInBetween,            isRedactorEnabled: u.mode !== "none"          }), nt.setContext("settings", {            ...c          }), nt.setContext("demo", {            ...l          }), nt.setContext("redactor", {            ...u          });          const f = new Error(o);          f.name = s, nt.captureException(f)        }).catch(a => {          console.error("Error while getting data for Sentry", a)        });        else {          const a = So(o) ? o : {              extraData: o            },            c = bn(a, ["slug"]);          nt.addBreadcrumb({            data: c,            message: s,            level: r,            category: n.options.defaults.tag          })        }      }    })  }

External destination receiving the log batch

    • s1270069.eu-nbg-2.betterstackdata.com

    BetterStack log-ingestion endpoint that receives the batched recording logs, including tab URL and account workspace context.

Where it sends data

Destinations our analysis observed Supademo contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.

  • ca.media.supademo.com

    Supademo sends data to ca.media.supademo.com. No other extension we have analyzed sends data here.

  • eu.media.supademo.com

    Supademo sends data to eu.media.supademo.com. No other extension we have analyzed sends data here.

  • media.supademo.com

    Supademo sends data to media.supademo.com. No other extension we have analyzed sends data here.

  • s1270069.eu-nbg-2.betterstackdata.com (BetterStack)

    Supademo sends data to s1270069.eu-nbg-2.betterstackdata.com (BetterStack). Named as a recipient in this extension's own analysis.

Updated 2 November 2026jblbcpkejogbghfdglhfjlplchcnmohm