Is Video Screenshot safe?

Medium risk

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.…

Video Screenshotv7.0.7Chrome Web Store
49Risk

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
What actually happens
You did this

You take a screenshot from a video page.

The click path and keyboard shortcut both reach the same screenshot completion message.

The extension did this

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.

Captured request
POSThttps://www.google-analytics.com/mp/collect?api_secret=<redacted>&measurement_id=G-6E5Z57BM0Y

204 No Content for one observed request; two repeated attempts to the same endpoint were aborted.

Headers
Content-Type
application/json
Body
{  "client_id": "8121c3d8-4766-437f-844b-ccc67bf4b1b3",  "events": [    {      "name": "Extension Popup",      "params": {        "action": "Opened",        "URL": "ppkojackhibeogijphhfnamhemklmial"      }    }  ]}
Fields assembled for the Google Analytics event
  • Persistent client ID
    8121c3d8-4766-437f-844b-ccc67bf4b1b3

    Lets analytics connect screenshot-related events from the same browser installation over time.

  • Page hostname
    www.youtube.com

    Shows which site was open when the extension event was generated.

  • Event name
    ScreenshotTaken

    Describes which extension event happened.

  • Action label
    Clicked

    Distinguishes whether the screenshot came from a click or keyboard shortcut.

  • Measurement ID
    G-6E5Z57BM0Y

    Routes the event to the extension operator’s Google Analytics property.

The code that does this

Screenshot and analytics code path

Readable version

Content script sends ScreenshotComplete after capture

deobfuscated/js/page.js
class 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&nbsp;<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&nbsp;<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.js
class 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.js
async 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) {})    }
External service receiving the event
    • 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
What actually happens
You did this

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 extension did this

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.

Storage fields reachable through the external message path
  • Persistent installation ID
    8121c3d8-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 count
    42

    Shows how many screenshots the extension has recorded locally.

  • Extension settings
    image/jpeg, askToSave=true, saveFolder="Video Screenshots"

    Can change how and where the extension captures or saves screenshots.

What's stored on your device

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
Contents
{  "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}
The code that does this

External message listener and storage helper

Readable version

Manifest external origins

deobfuscated/manifest.json
"externally_connectable": {"matches":["*://*.videoscreenshot.app/*","*://localhost/*"]}

Storage helper reads and writes chrome.storage.local

deobfuscated/js/background.js
class 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.js
class _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.js
async 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);}
Origins allowed to use the external message path
    • 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.

Reproduce it yourself

Checks whether a manifest-allowed page can request extension storage and then perform a harmless write/read round trip.

Requires
  • Chrome with Video Screenshot installed
  • A page origin allowed by the extension manifest
videoscreenshot-storage-message.js · js
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);})();
How to run it
  1. 1Run this from DevTools on a page whose origin matches https://*.videoscreenshot.app/* or http://localhost/* while the extension is installed.
Updated 30 September 2026ppkojackhibeogijphhfnamhemklmial