Is Video Screenshot safe?
Video Screenshot is medium risk. Taking a screenshot makes Video Screenshot's code send a GA event with the tab's hostname and a persistent per-install client ID. DA captured the same endpoint/body for a popup event; the code calls the same sender for screenshots.…
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Screenshot hostnames sent to Google Analytics
Taking a screenshot makes Video Screenshot's code send a GA event with the tab's hostname and a persistent per-install client ID.
DA captured the same endpoint/body for a popup event; the code calls the same sender for screenshots.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You take a screenshot from a video page.
The click path and keyboard shortcut both reach the same screenshot completion message.
The extension prepares a Google Analytics event tied to that page hostname.
The event body includes a persistent client ID from extension storage and the hostname derived from the sender tab URL.
204 No Content for one observed request; two repeated attempts to the same endpoint were aborted.
- Content-Type
- application/json
{ "client_id": "8121c3d8-4766-437f-844b-ccc67bf4b1b3", "events": [ { "name": "Extension Popup", "params": { "action": "Opened", "URL": "ppkojackhibeogijphhfnamhemklmial" } } ]}- Persistent client ID8121c3d8-4766-437f-844b-ccc67bf4b1b3
Lets analytics connect screenshot-related events from the same browser installation over time.
- Page hostnamewww.youtube.com
Shows which site was open when the extension event was generated.
- Event nameScreenshotTaken
Describes which extension event happened.
- Action labelClicked
Distinguishes whether the screenshot came from a click or keyboard shortcut.
- Measurement IDG-6E5Z57BM0Y
Routes the event to the extension operator’s Google Analytics property.
Screenshot and analytics code path
Content script sends ScreenshotComplete after capture
deobfuscated/js/page.jsclass fs{constructor(t){n(702),this.currentSite=t,this.pageMonitor=null,this.buttonMonitor=null,this.titleMonitor=null,this.capture=null,this.shortcutHandler=null;try{this.init()}catch(t){So.report(t)}}init(){var t=this.handlePageUpdate.bind(this),e=this.handleButtonClick.bind(this);this.pageMonitor=new $o(t),this.buttonMonitor=new ns(this.currentSite,e),this.titleMonitor=new ms(this.currentSite),os.setSite(this.currentSite),new As(this.takeScreenshot.bind(this))}handlePageUpdate(){try{this.buttonMonitor&&this.buttonMonitor.update(),this.titleMonitor&&this.titleMonitor.update()}catch(t){So.report(t)}}handleButtonClick(t){this.takeScreenshot(t,"Clicked")}async takeScreenshot(t,e){So.breadcrumb({category:"screenshot-capture",message:"takeScreenshot.activationMethod",data:{activationMethod:e},level:"info"});try{await hs(this.currentSite);let n=new zo(this.currentSite,t);if(null===n.video)throw new Error("No Video Found.");this.capture=new ds(this.currentSite),await this.capture.go(n,this.buttonMonitor,this.titleMonitor.title),n.playVideo(),window.chrome.runtime.sendMessage({type:"ScreenshotComplete",event:"ScreenshotTaken",action:e}),ps()}catch(t){await new Promise(t=>setTimeout(t,100)),t.message?.includes("Extension context invalidated")?new os("fail","Refresh this page to take a screenshot.",!0):t.message?.includes("PremiumRequired")?new os("fail",'Taking screenshots with this website requires a premium subscription. Upgrade <a href="#" class="vsUpgrade" target="_blank">here</a>.',!0):(new os("fail",'Oh no! There was an error taking the screenshot. Please refresh the page and try again or submit a <a href="#" class="vsReportIssue" target="_blank">bug report</a>.',!0),So.report(t)),document.querySelector(".vsUpgrade")?.addEventListener("click",async t=>{t.preventDefault(),chrome.runtime.sendMessage(chrome.runtime.id,{type:"newTab",object:{active:!0,url:"settings.html#/subscription"}})}),document.querySelector(".vsReportIssue")?.addEventListener("click",async t=>{t.preventDefault();let e=await new Promise(t=>{chrome.runtime.sendMessage(chrome.runtime.id,{type:"getTabID"},e=>{t(e)})});chrome.runtime.sendMessage(chrome.runtime.id,{type:"newTab",object:{active:!0,url:` settings.html # / troubleshoot ? referrer = $ { e } `}})})}}disable(){this.pageMonitor.remove(),this.buttonMonitor.delete()}}Background handler calls the analytics sender
deobfuscated/js/background.jsclass mt { constructor() { this.init() } init() { chrome.runtime.onMessage.addListener((e, t, n) => { switch (e.type) { case "ScreenshotDownload": l(e); break; case "CopyToClipboardToBackground": d(e); break; case "TabCaptureScreenshot": return chrome.tabs.captureVisibleTab(function(e) { n({ imgSrc: e }) }), !0; case "ScreenshotComplete": !async function(e, t) { chrome.storage.local.get(["screenshotCount"], function(e) { let t = Number(e.screenshotCount) + 1; chrome.storage.local.set({ screenshotCount: t }) }), p(e, t), chrome.storage.local.get(["sub"], function(e) { let t = e.sub; !0 === t.premium && t.renewal < Date.now() && ft("Screenshot taken but billing period ended.") }) }(e, t); break; case "GAEvent": p(e, t) } }) } }Analytics sender builds the GA4 POST body
deobfuscated/js/background.jsasync function p(e, t) { let n = await new Promise(e => { chrome.storage.local.get(["id"], async function(t) { e(t.id) }) }), r = new URL("https://www.google-analytics.com/mp/collect"); r.searchParams.append("api_secret", "<redacted>"), r.searchParams.append("measurement_id", "G-6E5Z57BM0Y"); let s = { client_id: n, events: [{ name: e.event, params: { action: e.action, URL: new URL(t.url).hostname } }] }; fetch(r, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(s) }).then(function(e) {}) }- www.google-analytics.com
Receives Measurement Protocol events containing the extension client ID, event name, action, and hostname parameter.
Video Screenshot site can read and write extension storage
Pages on videoscreenshot.app can send external messages to the extension's service worker.
Code shows these can call getStorage to read chrome.storage.local, and setStorage to overwrite account, subscription, ID, and settings fields.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You load a videoscreenshot.app page that can talk to the installed extension.
The manifest also allows localhost, which is commonly used for local development.
The service worker lets that page read or overwrite extension storage.
The listener passes requested keys directly to chrome.storage.local.get and supplied objects directly to chrome.storage.local.set.
- Persistent installation ID8121c3d8-4766-437f-844b-ccc67bf4b1b3
Lets extension-side account or usage records be tied back to the same browser installation.
- User account object{"uid":"u_9a4b2c7d","email":"viewer@example.com"} (illustrative)
Can contain account profile context returned by the extension subscription flow.
- Subscription object{"premium":true,"renewal":1783830790524} (illustrative)
Shows paid-plan state and renewal timing stored by the extension.
- Screenshot count42
Shows how many screenshots the extension has recorded locally.
- Extension settingsimage/jpeg, askToSave=true, saveFolder="Video Screenshots"
Can change how and where the extension captures or saves screenshots.
This record shows account, subscription, ID, usage, and settings fields the code stores and exposes via the external message handler.
- Location
- chrome.storage.local fields initialized by js/background.js
{ "id": "8121c3d8-4766-437f-844b-ccc67bf4b1b3", "sub": { "premium": true, "renewal": 1783830790524 }, "user": { "uid": "u_9a4b2c7d", "email": "viewer@example.com" }, "reviewed": false, "websites": [ { "name": "youtube", "tier": "basic", "enabled": true }, { "name": "netflix", "tier": "premium", "enabled": true } ], "askToSave": false, "blacklist": [], "imageType": "image/jpeg", "lastError": "", "whitelist": [], "saveFolder": "", "hideAllVSSIcons": false, "screenshotCount": 42, "lastReviewPrompt": 1783830790524, "showReviewPrompt": false}External message listener and storage helper
Manifest external origins
deobfuscated/manifest.json"externally_connectable": {"matches":["*://*.videoscreenshot.app/*","*://localhost/*"]}Storage helper reads and writes chrome.storage.local
deobfuscated/js/background.jsclass gt { constructor() { this.extensionId = chrome.runtime.id } async isInstalled() { return !!chrome.runtime && (!!chrome.runtime.id || await new Promise(e => { try { chrome.runtime.sendMessage(this.extensionId, { type: "installedCheck" }, function(t) { e(t) }) } catch (t) { e(!1) } })) } async getStorage() { let e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : null; return await new Promise(t => { chrome && chrome.runtime && chrome.storage ? chrome.storage.local.get(e, async function(e) { 1 === Object.values(e).length ? t(Object.values(e)[0]) : t(e) }) : chrome.runtime.sendMessage(this.extensionId, { type: "getStorage", keys: e }, function(e) { t(e) }) }) } async setStorage(e) { return await new Promise(t => { chrome?.storage?.local ? chrome.storage.local.set(e, function() { t("done!") }) : chrome.runtime.sendMessage(this.extensionId, { type: "setStorage", object: e }, function(e) { t(e) }) }) } async manifestVersion() { return await new Promise(e => { chrome?.runtime?.getManifest ? e(chrome.runtime.getManifest().version) : chrome.runtime.sendMessage(this.extensionId, { type: "manifest" }, function(t) { e(t) }) }) } async keyboardShortcut() { return await new Promise(e => { chrome?.commands ? chrome.commands.getAll(function(t) { for (let n of t) "screenshot-keyboard-shortcut" === n.name && e(n.shortcut) }) : chrome.runtime.sendMessage(this.extensionId, { type: "keyboardShortcut" }, function(t) { e(t) }) }) } async getTabInfo(e) { return await new Promise(t => { window.chrome.tabs.sendMessage(Number(e), { action: "GetTroubleshootData" }, e => { chrome.runtime.lastError && t(null), t(e) }) }) } newTab(e) { chrome.tabs ? chrome.tabs.create(e) : chrome.runtime.sendMessage(this.extensionId, { type: "newTab", object: e }) } }External message listener dispatches getStorage and setStorage
deobfuscated/js/background.jsclass _t { constructor() { chrome.runtime.onMessageExternal.addListener(async (e, t, n) => (new Promise(async t => { let r, s; switch (e.type) { case "installedCheck": n(!0); break; case "getStorage": n(await (new gt).getStorage(e.keys)); break; case "setStorage": r = await (new gt).setStorage(e.object); break; case "manifest": r = await (new gt).manifestVersion(); break; case "keyboardShortcut": r = await (new gt).keyboardShortcut(); break; case "newTab": chrome.tabs.create(e.object); break; case "goToAccountPage": s = await this.getTabID(), await chrome.tabs.update(s, { url: "chrome-extension://ppkojackhibeogijphhfnamhemklmial/settings.html#/subscription" }); break; case "goToSettingsPage": s = await this.getTabID(), await chrome.tabs.update(s, { url: "chrome-extension://ppkojackhibeogijphhfnamhemklmial/settings.html#/general" }) } n(r) }), !0)) } async getTabID() { return await new Promise(e => { chrome.runtime.sendMessage(chrome.runtime.id, { type: "getTabID" }, t => { e(t) }) }) } }Storage fields initialized by the background script
deobfuscated/js/background.jsasync function initializeStorage(forceDefaults) { const existing = await new Promise(resolve => { chrome.storage.local.get(null, async function(values) { resolve(values) }) }); const stored = { id: existing.id || c(), imageType: existing.imageType || "image/jpeg", whitelist: existing.whitelist || [], blacklist: existing.blacklist || [], saveFolder: existing.saveFolder || "", askToSave: true === existing.askToSave || "true" === existing.askToSave, hideAllVSSIcons: true === existing.hideAllVSSIcons || "true" === existing.hideAllVSSIcons, screenshotCount: existing.screenshotCount || 0, reviewed: true === existing.reviewed || "true" === existing.reviewed, showReviewPrompt: true === existing.showReviewPrompt || "true" === existing.showReviewPrompt, lastReviewPrompt: existing.lastReviewPrompt || Date.now(), user: existing.user || {}, sub: existing.sub || {}, lastError: existing.lastError || "" }; chrome.storage.local.set(stored);}- videoscreenshot.app
Manifest-allowed site pages can call the extension external messaging listener.
- localhost
Manifest-allowed local pages can call the same listener when running on localhost.
Checks whether a manifest-allowed page can request extension storage and then perform a harmless write/read round trip.
- Chrome with Video Screenshot installed
- A page origin allowed by the extension manifest
const EXTENSION_ID = 'ppkojackhibeogijphhfnamhemklmial';function send(message) { return new Promise((resolve, reject) => { chrome.runtime.sendMessage(EXTENSION_ID, message, response => { const err = chrome.runtime.lastError; if (err) reject(new Error(err.message)); else resolve(response); }); });}(async () => { const before = await send({ type: 'getStorage', keys: ['id', 'user', 'sub', 'screenshotCount'] }); console.log('Readable storage fields:', before); const marker = 'probe-' + Date.now(); await send({ type: 'setStorage', object: { storageProbe: marker } }); const after = await send({ type: 'getStorage', keys: ['storageProbe'] }); console.log('Writable storage probe:', after);})();- 1Run this from DevTools on a page whose origin matches https://*.videoscreenshot.app/* or http://localhost/* while the extension is installed.