Is Guidde - Magically create video documentation safe?

High risk

Guidde is high risk. Guidde posts analytics events to app.guidde.com/tk/v1/track for extension and web-app actions, with account properties (user ID, email, name, org ID) plus event data that can include the current page URL.…

Guidde Chrome Storev89.0.0Chrome Web Store
72Risk
Who publishes it

Guidde Inc - no other listings under this identity, 1 shared hostname

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

Publisher
Guidde Chrome Store
Declared legal entity
Guidde Inc
Registered address
2031 Lyon Ave, Belmont, CA 94002-1636, US
Registered contact
Yoav

Shared hosts - 1 hostname

Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.

api.stigg.io
Also called by 4 other listings, including Wordtune, Wordtune: AI Writing, Paraphrasing & Grammar Tool

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

Publishers can request a review.

Findings

Guidde posts tracked user events and page URLs to its backend

Guidde posts analytics events to app.guidde.com/tk/v1/track for extension and web-app actions, with account properties (user ID, email, name, org ID) plus event data that can include the current page URL.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You use a Guidde extension feature or trigger an extension lifecycle event.

Examples in the source include opening recording UI, welcome-page actions, upload failures, and step-limit events.

The extension did this

Guidde sends an analytics event that can combine your account identity with page context.

The request goes to Guidde's own tracking endpoint under app.guidde.com.

Captured request
POSThttps://app.guidde.com/tk/v1/track

Observed during dynamic analysis; the preserved evidence identified request-body keys but did not preserve a full body.

Fields the analytics helper places in the tracking request
  • User ID
    user-id: 8f4c9e31-7b6a-4f4d-9c1d-0c2a7d6e5a19

    Links the tracked event to your signed-in Guidde account.

  • Email address
    email: alex.rivera@example.com

    Connects the event to an address that can identify you directly.

  • Name
    name: Alex Rivera

    Adds your display name to the analytics record when the account has one.

  • Organization ID
    organization-id: org_6b2c91f4

    Associates the event with your workspace or organization inside Guidde.

  • Page URL
    https://app.guidde.com/signup?src=extension

    Shows which page or Guidde screen was associated with the event.

  • Extension metadata
    ext-version: 81.0.0

    Adds the extension ID, version, browser, and environment to help group your event.

The code that does this

The tracking helper builds the analytics payload and posts it to Guidde

Readable version

Readable analytics properties and tracking request

background.js
const aZ = () => ({  ...oZ(),  ...sZ(),  source: "chrome-extension",  "ext-id": chrome.runtime.id,  "ext-version": nZ(),  environment: Li.firebase.projectId}),cZ = t => t ? {  ...t.customProps || {},  "user-id": t.user.uid,  email: t.user.email,  name: t.user.displayName,  "organization-id": t.roles?.o} : {},_g = async () => (await chrome?.storage?.local.get([oa]))[oa] || Li.firebase.authDomain,fa = async () => `https://${await _g()}`;let M0 = {},  L0 = {};const uZ = (t, e = {}) => ds("/tk/v1/track", "POST", {    userProperties: M0,    globalProperties: L0,    eventData: {      event: t,      ...e,      timeMs: new Date().getTime()    }  }),  ln = (t, e = {}, n = null) => {    const r = n ? {        url: n.url      } : {},      i = {        ...e,        ...r      };    return uZ(t, i)  }
Destination receiving these events
    • app.guidde.com

    Guidde backend endpoint that receives /tk/v1/track analytics posts from the extension.

Guidde recording uploads screenshots, URLs, and DOM events

Recording a Guidde flow makes the content script listen for interactions, build a step event with the page URL and DOM details, and ask the background to screenshot.

It uploads via a signed URL and posts the step to Guidde's addStep API.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You start a Guidde recording session on a page.

The recording path is tied to the quick-guide workflow rather than every browsing session.

The extension did this

The extension records step metadata, captures screenshots, and uploads the resulting step to Guidde.

The code path includes both a screenshot upload and an addStep API call.

Fields collected for a recorded step
  • Page URL
    https://app.guidde.com/welcome

    Shows the page where your recorded step happened.

  • Tab title
    Welcome to Guidde

    Adds the visible page title to the recorded step.

  • DOM node details
    tagName: INPUT, type: text, name: email

    Describes the clicked or typed-into page element, which can reveal form labels or element identifiers.

  • Window dimensions
    innerWidth: 1440, innerHeight: 900

    Adds screen context that helps reconstruct where the step occurred on your page.

  • Screenshot reference
    quickguiddeScreenshots/uid_8f4c9e31/playbook_20260712/playbook_20260712_4d9f0c2a.png

    Links the step to a captured image of the visible tab during recording.

Captured request
POSThttps://app.guidde.com/c/v1/ext/quickguidde/addStep

The source builds this POST during recording; the preserved traffic evidence did not capture a full request body for this path.

The code that does this

The content script turns page interactions into recording events

Readable version

Readable event collection and NEW_EVENT posting

iframeCS.js
const nL = (t, e, n, r, i, s = []) => {  let u;  try {    u = {      innerHeight: top?.window?.innerHeight || window.innerHeight,      innerWidth: top?.window.innerWidth || window.innerWidth,      outerHeight: window.outerHeight,      outerWidth: window.outerWidth    }  } catch {    u = {      innerHeight: window?.innerHeight,      innerWidth: window?.innerWidth,      outerHeight: window.outerHeight,      outerWidth: window.outerWidth    }  }  let p = {    x: 0,    y: 0  };  r instanceof MouseEvent && (p = {    x: r.clientX,    y: r.clientY  });  const g = (I, O) => {    p.x = I.clientX + O.getBoundingClientRect()?.x, p.y = I.clientY + O.getBoundingClientRect()?.y  };  let h = !1;  if (self !== top) {    const I = r?.view?.frameElement;    if (I) h = !0, g(r, I);    else {      const O = document?.activeElement,        x = r?.currentTarget?.defaultView?.frameElement;      x ? (h = !0, ql = x, p.x = O?.getBoundingClientRect().x + x.getBoundingClientRect().x, p.y = O?.getBoundingClientRect().y + x.getBoundingClientRect().y) : ql && (h = !0, g(r, ql))    }  } else h = !0;  const b = s?.[0] instanceof Element ? s[0] : null,    S = r?.target instanceof Element ? r.target : null;  return {    screenshot: null,    id: S?.id,    node: CI({      type: e,      stepTextLimit: n,      event: r,      title: i,      isFrameAccessible: h,      clickedItem: S    }),    composedPathNode: CI({      type: e,      stepTextLimit: n,      event: r,      title: i,      isFrameAccessible: h,      clickedItem: b    }),    devicePixelRatio: window.devicePixelRatio,    url: window.location.href,    timeStamp: t,    type: e,    windowDimensions: u,    mousePosition: p  }},Vl = ({  port: t,  timeStamp: e,  type: n,  event: r,  composedPath: i,  title: s = ""}) => {  jM(u => {    const {      useTabUrl: p,      tabUrl: g    } = u, h = SI();    t.postMessage({      action: "CAPTURE_NOW",      uuid: h,      url: p ? g : window.location.href,      type: n    }), t.postMessage({      action: "NEW_EVENT",      uuid: h,      url: p ? g : window.location.href,      QGEvent: nL(e, n, u.stepTextLimit, r, s, i)    })  })}

Readable listeners for recording events

iframeCS.js
const Cv = new lG("iframeCS"),  dG = sC((t, e, n, r) => {    Vl({      timeStamp: r,      type: "input",      port: t,      event: e,      composedPath: n    })  }, 500),  fG = 5e3,  Lm = {    timestamp: 0,    target: null  },  Vf = {    mousedown: (t, e) => {      e.composedPath().some(r => r instanceof HTMLElement ? r.id === F_ || r.id === Wl : !1) || (t.postMessage({        action: "COUNT_EVENT",        type: "mousedown",        url: window.location.href      }), rL(Date.now(), t, e, e.composedPath(), !0))    },    click: t => {      t.postMessage({        action: "COUNT_EVENT",        type: "click",        url: window.location.href      })    },    keyup: (t, e) => {      if (e.key === "Enter") {        if (e.isComposing || e.target === Lm.target && Date.now() - Lm.timestamp < fG) return;        Vl({          port: t,          event: e,          timeStamp: Date.now(),          type: "freeText",          title: "Go here",          composedPath: e?.composedPath()        })      }    },    input: (t, e) => {      e.composedPath().some(r => r instanceof HTMLElement ? r.id === F_ || r.id === Wl : !1) || (Lm.timestamp = Date.now(), Lm.target = e.target, dG(t, e, e.composedPath(), Date.now()))    }  }
The code that does this

The background script captures screenshots and posts addStep

Readable version

Readable upload helpers

background.js
const Zme = (t, e) => fetch(t, {  method: "PUT",  body: typeof e == "string" ? Jme(e) : e}).then(n => {  if (!n.ok) throw new Error(`Upload failed with status ${n.status}: ${n.statusText}`)}),ege = t => t.replace("storage.googleapis.com", `gstorage.${Li.firebase.authDomain}`),tge = (t, e = !0) => ds("/c/v1/get-upload-url", "POST", {  filename: t}).then(n => ({  url: e ? ege(n.url) : n.url})),nge = (t, e) => new Promise((n, r) => {  const i = "gs:///" + t;  Ie.info("Getting signed URL", Oe.CreateQgFlow), tge(i).then(({    url: s  }) => {    Ie.info("Signed URL received, uploading file", Oe.CreateQgFlow), Zme(s, e).then(() => {      Ie.info("File uploaded successfully", Oe.CreateQgFlow), n(i)    }).catch(o => {      Ie.error("File upload failed", Oe.CreateQgFlow, {        error: o      }), ln("file_upload_failed", {        storagePath: t      }), Bt(o), r(o)    })  }).catch(s => {    Bt(s), r(s)  })}),rge = (t, e) => ds("/c/v1/ext/quickguidde/addStep", "POST", {  playbookId: t,  step: e}).then(n => (Ie.info("addStep POST request completed successfully", Oe.CreateQgFlow, {  stepId: e.id}), n)).catch(n => {  Ie.error("addStep POST request failed", Oe.CreateQgFlow, {    stepId: e.id,    error: n  })})

Readable addMetaData/addStep methods

background.js
async addMetaData(e, n, r, i, s, o, c) {  Ie.info("Adding metadata", Oe.CreateQgFlow, {    stepWithoutScreenshot: e  });  const [l, d] = await Promise.race([Promise.all([this.getStepElapsedTimeInRecording(e.timeStamp), this.getScreenDimensionFromTab(e.windowDimensions)]), new Promise(_ => setTimeout(() => _([0, null]), 2e3))]);  d && (e.windowDimensions.innerHeight = d.innerHeight, e.windowDimensions.innerWidth = d.innerWidth), e.type === "input" ? (this.aggregatedStep && EG(e.node) !== EG(this.aggregatedStep.eventData.node) && this.processAggregatedStep(), this.aggregatedStep = {    uuid: n,    index: r,    url: i,    favicon: s,    tabTitle: o,    eventData: e,    processed: !1,    ...this.captureVideo ? {      stepTimeInRecording: l,      tabId: c    } : {}  }) : (this.processAggregatedStep(), this.data.addEventData({    uuid: n,    index: r,    url: i,    favicon: s,    tabTitle: o,    eventData: e,    processed: !1,    ...this.captureVideo ? {      stepTimeInRecording: l,      tabId: c    } : {}  }), this.data.isEventReady(n) && this.addStep(this.data.getItem(n), n))}addStep(e, n) {  if (Ie.info("Adding step", Oe.CreateQgFlow), !this.playbookId) throw new Error("Cannot add QG steps - video doesn't exist");  this.data.markItemAsProcessed(n);  const r = {      ...e.eventData,      screenshot: e.imageUrl,      id: e.uuid,      favicon: e.favicon,      documentTitle: e.tabTitle,      stepIndex: this.steps.length,      ...this.captureVideo ? {        relativeRecordingTime: e.relativeRecordingTime,        tabId: e.tabId      } : {}    },    i = rge(this.playbookId, r);  this.steps.push(i), this.lastStepUrl = e.eventData.url}

Readable screenshot capture method

background.js
captureNow({  uuid: e,  url: n,  type: r,  favicon: i,  tabTitle: s}) {  Ie.info("Requesting browser engine to capture image", Oe.CreateQgFlow, {    eventType: r  });  const o = this.imageFormat === "jpeg" || this.imageFormat === "png" ? this.imageFormat : "png",    c = this.imageQuality >= 0 && this.imageQuality <= 100 ? this.imageQuality : 50;  (this.imageFormat !== o || this.imageQuality !== c) && (Ie.warn("Invalid capture parameters detected and corrected", Oe.CreateQgFlow, {    originalFormat: this.imageFormat,    safeFormat: o,    originalQuality: this.imageQuality,    safeQuality: c  }), Bt(new Error("Invalid captureVisibleTab parameters detected"))), this.data.addImageUrl(e, "loading", this.stepsIndex, n, i, s, !1, !1), chrome.tabs.captureVisibleTab({    format: o,    quality: c  }, l => {    if (Ie.info("image captured", Oe.CreateQgFlow), chrome.runtime.lastError) {      Ie.info("Runtime had error", Oe.CreateQgFlow, {        error: chrome.runtime.lastError      }), Bt(chrome.runtime.lastError), this.data.addImageUrl(e, null, this.stepsIndex, n, i, s, !0, !0), ln("step_dropped", {        playbookType: "QG",        uuid: e,        url: n,        playbookId: this.playbookId,        lastError: JSON.stringify(chrome.runtime.lastError)      });      return    }    r !== "input" && this.incrementRecPanelCounter();    const {      uid: d    } = Si()?.user, _ = `quickguiddeScreenshots/${d}/${this.playbookId}/${this.playbookId}_${e}.${o}`;    Ie.info("Uploading image to storage", Oe.CreateQgFlow, {      storageRef: _    }), nge(_, l).then(E => {      this.data.addImageUrl(e, E, this.stepsIndex, n, i, s, !1, !1), setTimeout(() => {        const S = this.data.getItem(e);        Ie.info("Image uploaded. Attempting to add a step", Oe.CreateQgFlow), S && !S.processed && this.data.isEventReady(e) && this.addStep(S, e)      }, 500)    }).catch(E => {      ln("screenshot_upload_failed", {        playbookType: "QG",        uuid: e,        url: n,        playbookId: this.playbookId,        storagePath: _,        error: E?.message || String(E)      }), Bt(E, {        context: "screenshot_upload_failed",        uuid: e,        playbookId: this.playbookId,        storageRef: _      }), this.data.addImageUrl(e, null, this.stepsIndex, n, i, s, !0, !0)    })  })}
Destinations used by the recording flow
    • app.guidde.com

    Guidde API host that receives addStep and signed-upload-url requests.

    • gstorage.app.guidde.com

    Guidde storage hostname used for screenshot uploads through signed PUT URLs.

Updated 30 September 2026oacmmmjedhheaijfjidilonpngccnhdl