Is Kommodo Screen Recorder & SOP safe?

Medium risk

Kommodo is medium risk. Starting a Kommodo recording has the content script tell the background worker to prepare with the page title; the recorder requests screen sharing, and the worker can take PNG screenshots of the visible tab. Uploads go to Kommodo's API.…

kgrauerv3.4.1Chrome Web Store
49Risk
Who publishes it

Komodo Technologies - 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
kgrauer
Declared legal entity
Komodo Technologies
Registered address
200 Undercliff Ave, Edgewater, NJ 07020-1901, US
Registered contact
Khanan Grauer
Website

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

Publishers can request a review.

Findings

Recording sessions upload screen video and tab screenshots

Starting a Kommodo recording has the content script tell the background worker to prepare with the page title; the recorder requests screen sharing, and the worker can take PNG screenshots of the visible tab.

Uploads go to Kommodo's API.

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

You start a recording session from the extension's on-page controls.

The code path begins when the start-recording button calls the recording-prepare message.

The extension did this

The extension requests screen recording, can take a visible-tab screenshot, and routes recording assets to the upload service.

The manifest also loads the main content script on all URLs, so the recording UI can run across sites.

Recording-session data exposed by the code path
  • Screen video
    A web app dashboard visible during recording (illustrative)

    The recording can include whatever is visible in the selected screen or tab during the session.

  • Visible-tab screenshot
    PNG image of the active browser tab (illustrative)

    A screenshot can show page content, account context, and document details visible in the active tab.

  • Page title
    Customer Account Settings - Example CRM (illustrative)

    The title can reveal the document, account area, or workflow being recorded.

  • Page address and host
    https://app.example.com/customers/1842 (illustrative)

    Guide steps preserve the address and hostname for pages involved in the recording workflow.

Captured request
POSThttps://api.prod.komododecks.com/api/v2/uploads

The code constructs recording-upload link requests from API_CLOUDFLARE; no recording-upload request body was recorded in the available traffic evidence.

The code that does this

Recording starts on the page, records display media, captures tab images, and configures API uploads

Readable version

Content script starts recording with the current page title

dist/cs/rl-cs.js
clickStartScreenRec() {  this.getMainDiv() && csRoot.ccomm.bgMessage({    type: n.MSGCS_UI_REC_PREPARE,    data: {      title: document.title    }  })}

Offscreen recorder requests screen sharing and creates a MediaRecorder

dist/bg/offscreen/rl-offscreen.js
async getVideoStream(e) {  const t = {    video: {      displaySurface: "monitor",      height: {        max: e      }    },    audio: {      autoGainControl: !1,      echoCancellation: !1,      noiseSuppression: !1    },    systemAudio: "include"  };  let r = null;  try {    r = await navigator.mediaDevices.getDisplayMedia(t)  } catch (e) {    throw "NotAllowedError" === e.name || e.message.startsWith("Permission denied") ? "Permission denied by user" === e.message ? new this.DeviceCaptureCanceled("Screen share canceled", {      cause: e    }) : new this.DeviceCaptureNotAllowed("Screen capture error: " + e.message, {      cause: e    }) : new this.DeviceCaptureError("Screen capture error: " + e.message, {      cause: e    })  }  return r?.getVideoTracks().forEach(e => {    e.contentHint = "motion"  }), r}const s = new MediaRecorder(e, {  mimeType: "video/webm;codecs=h264,opus"});return s.recorderKind = Hi.mediarecorder, s

Service worker captures the visible tab as PNG

dist/bg/sw/rl-sw.js
async function ui() {  let e = null;  try {    e = await chrome.tabs.captureVisibleTab(chrome.windows.WINDOW_ID_CURRENT, {      format: "png"    })  } catch (e) {    klog.warn("Error in captureImageFromVisibleTab:", e)  }  return e}

Upload service posts for upload links and sends binaries

dist/bg/sw/rl-sw.js
o = {  requestUploadLinks: async e => (await XT.post(e.endpoint, {}, {    headers: {      Authorization: `${e.authType} ${e.authToken}`    },    timeout: e.timeout  })).data,  async uploadBinary(e) {    const t = typeof XMLHttpRequest < "u" && e.onProgress ? t => {      if (t.total) {        const n = Math.round(t.loaded / t.total * 100);        e.onProgress({          loaded: t.loaded,          total: t.total,          percent: n        })      }    } : void 0;    await XT.put(e.url, e.data, {      headers: {        "Content-Type": e.contentType      },      timeout: e.timeout,      signal: e.signal,      onUploadProgress: t    })  },  async writeToFirestore(e) {    await XT.patch(e.url, e.data, {      headers: {        Authorization: `IdToken ${e.authToken}`,        "Content-Type": "application/json"      },      timeout: e.timeout,      signal: e.signal    })  }},c = function(e) {  return {    videoRecordLinks(t) {      const n = new URL(`${e}/v2/uploads/${t.uploadId}/record`);      return t.presentationId && n.searchParams.set("presentationId", t.presentationId), n.searchParams.set("presentationCount", String(t.count)), n.searchParams.set("presentationFormat", t.format), void 0 !== t.startFrom && n.searchParams.set("presentationStartFrom", String(t.startFrom)), n.toString()    },    videoMultipartLinks(t, n, r) {      const s = new URL(`${e}/v2/uploads/${t}/multipart`);      return s.searchParams.set("presentationId", n), s.searchParams.set("size", String(r)), s.toString()    }  }}

Guide recording uploads typed form input and page URLs

Typing into a form field while recording a guide makes the content script read the value, page URL, hostname, and element coordinates.

The service worker forwards the step to the offscreen recorder, which packages it for Kommodo's API.

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

You record a guide and finish a step after typing in a form field.

The trigger is tied to the extension's guide-recording state, not ordinary browsing outside that workflow.

The extension did this

The extension packages the typed value with the page address and sends it into the recording upload flow.

The same guide step object can also carry screenshot keys and element coordinates.

Guide-step fields assembled by the content script
  • Typed form value
    Quarterly budget notes (illustrative)

    Text you enter while recording can include names, notes, search terms, or other page-specific content.

  • Full page address
    https://app.example.com/accounts/1842/profile (illustrative)

    The page address can reveal the site and workflow you were recording.

  • Page hostname
    app.example.com (illustrative)

    The hostname identifies which service or internal web app was open during the step.

  • Element coordinates
    left 224, top 318, width 420, height 36 (illustrative)

    The element size data describes where on the page the recorded interaction happened.

Captured request
POSThttps://api.prod.komododecks.com/api/v1/links

The code constructs guide-upload link requests from API_CLOUDFLARE; no guide-upload request body was recorded in the available traffic evidence.

The code that does this

Guide input is read on the page, forwarded through the service worker, and stored as a guide step

Readable version

Content script sends textVal, location, url, and elementSizes

dist/cs/rl-cs.js
finishStep() {  if (this.inputActive) {    if (this.inputActive.value) {      const e = this.inputActive.value,        t = this.inputActive.getBoundingClientRect();      this.addStep(m.GUIDE_STEP_TYPE, "Guide___Type_userinput_", e, t)    }    this.inputActive.removeEventListener("blur", this._b_onBlur), this.inputActive = null  }},addStep(e, t, o, i, s) {  const r = {      hostname: window.location.hostname,      href: window.location.href    },    a = s || window.location.href;  if (csRoot.isIframe) {    const s = {      type: "KMD_CLICK_BUBBLE",      stepType: e,      textType: t,      textVal: o,      location: r,      url: a,      clickX: i.clickX,      clickY: i.clickY,      rect: {        width: i.width,        height: i.height,        top: i.top,        left: i.left      }    };    return void window.parent.postMessage(s, "*")  }  let d = null;  if (i) {    const e = window.devicePixelRatio;    d = {      clickX: i.clickX * e,      clickY: i.clickY * e,      width: i.width * e,      height: i.height * e,      top: i.top * e,      left: i.left * e,      right: (i.left + i.width) * e,      bottom: (i.top + i.height) * e    }  }  globalThis.csRoot && csRoot.ccomm && csRoot.ccomm.bgMessage({    type: n.MSGCS_GUIDE_CAPTURE,    data: {      stepType: e,      textType: t,      textVal: o,      location: r,      url: a,      elementSizes: d,      isIframe: !1    }  })}

Service worker adds timing and optional screenshot before forwarding

dist/bg/sw/rl-sw.js
case Xs.MSGCS_GUIDE_CAPTURE:  return void async function(e) {    const t = [];    t[0] = ii.offscreenRequest({      type: Ys.REQ_REC_TIME,      data: {        format: "ms"      }    }), e.stepType === gi.GUIDE_STEP_TYPE || e.stepType === gi.GUIDE_STEP_GO || (t[1] = ui());    const [n, r] = await Promise.all(t);    let s = null;    if (r) {      s = `guide:${_i.recNameReplica}:${Ks.generateRandomName()}`;      const e = wi.dataURLToBinaryBlob(r);      await Gi.saveBlob(s, e)    }    ii.offscreenMessage({      type: Ys.MSG_GUIDE_CAPTURE,      data: {        timerStr: n,        screenshotKey: s,        ...e      }    })  }(e.data);

Offscreen recorder preserves text, location, URL, image key, and element sizes

dist/bg/offscreen/rl-offscreen.js
case zi.MSG_GUIDE_CAPTURE:  return void async function(e) {    const {      timerStr: t,      screenshotKey: r,      stepType: n,      textType: i,      textVal: s,      location: o,      url: a,      elementSizes: c    } = e;    if (!us.recName || !ds(null)) return;    const d = String(s);    let u = "";    u = n === rs.GUIDE_STEP_CLICK ? d.length > 0 ? d.length > 100 ? `"${d.substring(0,100)+"…"}"` : `"${d}"` : await os("Guide___highlighted_element_") : n === rs.GUIDE_STEP_TYPE ? d.length > 1e3 ? `"${d.substring(0,1e3)+"…"}"` : `"${d}"` : d.length > 200 ? `"${d.substring(0,200)+"…"}"` : `"${d}"`;    const l = await os(i, [u]);    let h = {      guideLastStep: null    };    n === rs.GUIDE_STEP_TYPE && Object.assign(h, {      guideTempTimecode: null    });    const m = await ss.get(h),      p = m.guideLastStep;    let f = null;    if (n === rs.GUIDE_STEP_TYPE) {      const e = await Ps.getBlob("guide:temp_screenshot");      e && (f = `guide:${us.recName}:${Xi.generateRandomName()}`, await Ps.saveBlob(f, e), await Ps.deleteBlob("guide:temp_screenshot"))    }    f || (f = r);    const g = {      text: l,      location: o,      url: a    };    if (f && (g.imageKey = f), c && (g.elementSizes = c), us.recType === Wi.RECTYPE_VIDEO_AND_GUIDE && (m.guideTempTimecode ? g.timecode = m.guideTempTimecode : g.timecode = t), p?.location.hostname !== o.hostname && n !== rs.GUIDE_STEP_GO) {      const e = {        text: await os("Guide___Go_to_userlocation", [o.hostname]),        imageKey: null,        location: o,        url: o.href      };      us.recType === Wi.RECTYPE_VIDEO_AND_GUIDE && (e.timecode = p?.timecode ? p.timecode + 1e3 : 0), es.workerMessage({        type: zi.MSG_GUIDE_STEP_BRIDGED,        data: {          recType: us.recType,          recName: us.recName,          step: e        }      })    }    es.workerMessage({      type: zi.MSG_GUIDE_STEP_BRIDGED,      data: {        recType: us.recType,        recName: us.recName,        step: g      }    }), ss.set({      guideLastStep: g    })  }(e.data);
Updated 30 September 2026kodipildlgjdkoeokgibdlighekmebcg