Is Kommodo Screen Recorder & SOP safe?
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.…
Who publishes itKomodo Technologies - no other listings under this identity
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.
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
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 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.
- Screen videoA 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 screenshotPNG image of the active browser tab (illustrative)
A screenshot can show page content, account context, and document details visible in the active tab.
- Page titleCustomer Account Settings - Example CRM (illustrative)
The title can reveal the document, account area, or workflow being recorded.
- Page address and hosthttps://app.example.com/customers/1842 (illustrative)
Guide steps preserve the address and hostname for pages involved in the recording workflow.
The code constructs recording-upload link requests from API_CLOUDFLARE; no recording-upload request body was recorded in the available traffic evidence.
Recording starts on the page, records display media, captures tab images, and configures API uploads
Content script starts recording with the current page title
dist/cs/rl-cs.jsclickStartScreenRec() { 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.jsasync 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, sService worker captures the visible tab as PNG
dist/bg/sw/rl-sw.jsasync 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.jso = { 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
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 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.
- Typed form valueQuarterly budget notes (illustrative)
Text you enter while recording can include names, notes, search terms, or other page-specific content.
- Full page addresshttps://app.example.com/accounts/1842/profile (illustrative)
The page address can reveal the site and workflow you were recording.
- Page hostnameapp.example.com (illustrative)
The hostname identifies which service or internal web app was open during the step.
- Element coordinatesleft 224, top 318, width 420, height 36 (illustrative)
The element size data describes where on the page the recorded interaction happened.
The code constructs guide-upload link requests from API_CLOUDFLARE; no guide-upload request body was recorded in the available traffic evidence.
Guide input is read on the page, forwarded through the service worker, and stored as a guide step
Content script sends textVal, location, url, and elementSizes
dist/cs/rl-cs.jsfinishStep() { 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.jscase 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.jscase 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);