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.5.5Chrome Web Store
45Risk

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

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Recording Logs Send Tab URLs to BetterStack

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

Log entries include the recorded tab URL plus signed-in account/workspace context (userId, workspaceId, name), seen in the JSON body.

01EvidenceCAUSE EFFECT
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.

02EvidenceTEMPORAL PATTERN
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.

03EvidenceFIELD TABLE
Fields the logger adds to each BetterStack batch
FieldValueWhy it matters
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.
04EvidenceNETWORK CAPTURE
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-Typeapplication/json
AuthorizationBearer <redacted>
05EvidenceCODE COMPARE
The code that does this

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

What it actually does
content.js deobfuscated logger and sendercontent-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 sendercontent-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
          })
        }
      }
    })
  }
06EvidenceTHIRD PARTY LIST
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.

Data recipients

media.supademo.comeu.media.supademo.comca.media.supademo.coms1270069.eu-nbg-2.betterstackdata.com (BetterStack)
Updated 17 September 2026jblbcpkejogbghfdglhfjlplchcnmohm