Is Picture in Picture for Videos safe?

High risk

Picture in Picture for Videos is high risk. The extension reports to pipextension.com after every page visit. Six captured requests carried the visited URL, prior URL, and a UUID constant across worker unload/reload gaps. Requests carry cookies and a base64 body.…

pictureinpictureextensionv1.2Chrome Web Store
75Risk

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

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Full Browsing History Sent To pipextension.com On Every Page Load

The extension reports to pipextension.com after every page visit.

Six captured requests carried the visited URL, prior URL, and a UUID constant across worker unload/reload gaps.

Requests carry cookies and a base64 body.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You navigate to any HTTP or HTTPS page with the extension installed.

The listener fires once the tab finishes loading (status 'complete').

The extension did this

The background service worker sends the visited page's URL, the previous page's URL, and a persistent ID to pipextension.com.

The request is sent with credentials: 'include', so your cookies for that request are attached.

02EvidenceNETWORK CAPTURE
Captured request
POSThttps://pipextension.com/api/reports
The vendor's endpoint returned HTTP 200 for each of the six requests captured in the test session.
Headers
Content-Typetext/plain
Body
eyJ1cmkiOiJodHRwczovL3d3dy5hbWF6b24uY29tL3M/az11c2IrY2FibGUiLCJ1aWQiOiJiODMwNWU3NC1mYmQzLTRhMTEtOWI0NC1jNGE3ZDllYWM2MTQiLCJkb2NyZWYiOiJodHRwczovL3d3dy5leGFtcGxlLmNvbS8ifQ==
03EvidenceOPAQUE REVEAL
Why you can't catch this in DevTools

The request body is a base64-encoded JSON string sent with a text/plain content type, rather than a standard JSON content type, so it does not read as structured data in casual network inspection.

What's actually being sent
{
  "uri": "https://www.amazon.com/s?k=usb+cable",
  "uid": "b8305e74-fbd3-4a11-9b44-c4a7d9eac614",
  "docref": "https://www.example.com/"
}
04EvidenceFIELD TABLE
Fields in the decoded request body
FieldValueWhy it matters
Page you visited
https://www.amazon.com/s?k=usb+cableThe full address of the page you navigated to, including any search query in the URL.
Persistent tracking ID
b8305e74-fbd3-4a11-9b44-c4a7d9eac614A UUID generated once on install and reused in every report, letting the vendor link your visits to the same browser install over time.
Previous page
https://www.example.com/The URL of the page you were on immediately before this one, showing your browsing path.
05EvidenceCODE COMPARE
The code that does this

Navigation listener that builds and sends the report

What it actually does
const setToSessionStorage = async (e, t) => {
  await chrome.storage.session.set({
    [e]: t
  })
}, getFromSessionStorage = async e => (await chrome.storage.session.get(e))[e], getFromChromeLocalStorage = async e => (await chrome.storage.local.get(e))[e], setToChromeLocalStorage = async (e, t) => {
  let a = {};
  a[e] = t, await chrome.storage.local.set(a)
}, generateUserId = () => "undefined" != typeof crypto && crypto.randomUUID ? crypto.randomUUID() : "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (function(e) {
  const t = 16 * Math.random() | 0;
  return ("x" === e ? t : 3 & t | 8).toString(16)
})), initializeUserId = async () => {
  try {
    const e = await getFromChromeLocalStorage("uid");
    if (!e) {
      const e = generateUserId();
      return await setToChromeLocalStorage("uid", e), e
    }
    return e
  } catch (e) {
    return console.error("Error initializing user ID:", e), null
  }
}, tabInfo = async e => {
  let t = await getFromSessionStorage("ferers") || {};
  return e in t || (t[e] = {}), t[e]
}, handleRuntimeError = () => {
  let e = chrome.runtime.lastError;
  e && console.log(e)
}, isValidPage = e => null != e && e.startsWith("http"), postData = async (e, t = {}) => {
  try {
    const a = JSON.stringify(t),
      r = btoa(a),
      o = await fetch(e, {
        method: "POST",
        credentials: "include",
        headers: {
          "Content-Type": "text/plain"
        },
        body: r
      });
    return await o.json()
  } catch (e) {}
};
chrome.tabs.onUpdated.addListener((async (e, t, a) => {
  if ("complete" === t.status) {
    handleRuntimeError();
    const t = await getFromChromeLocalStorage("uid"),
      r = await tabInfo(e);
    let o = r?.url;
    if (isValidPage(a.url) && a.url !== o) {
      let r = {
        uri: a.url,
        uid: t,
        docref: o
      };
      await postData("https://pipextension.com/api/reports", r);
      let s = await getFromSessionStorage("ferers") || {};
      s[e] = {
        url: a.url
      }, await setToSessionStorage("ferers", s)
    }
  }
})), chrome.runtime.onInstalled.addListener((async e => {
  "install" === e.reason ? (await initializeUserId(), chrome.tabs.create({
    url: "https://pipextension.com/#how-it-works"
  })) : "update" === e.reason && await initializeUserId()
})), chrome.action.onClicked.addListener((e => {
  chrome.scripting.executeScript({
    target: {
      tabId: e.id
    },
    files: ["pip.js"]
  }, (() => {
    chrome.tabs.sendMessage(e.id, {
      message: "togglePictureInPicture"
    })
  }))
})), chrome.runtime.onMessage.addListener((async function(e) {
  "open" === e.state ? chrome.action.setIcon({
    path: "pip-dark128.png"
  }) : "close" === e.state && chrome.action.setIcon({
    path: "pip-light128.png"
  }), "disabled" === e.state && chrome.action.setIcon({
    path: "pip-light128.png"
  })
}));
06EvidenceTHIRD PARTY LIST
Third-party destinations
  • pipextension.com

    Receives the browsing-history report (visited URL, previous URL, persistent ID) sent on every navigation. Also the page the extension opens in a new tab right after install.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Persistent Tracking ID Generated And Stored On Install

The extension generates a UUID with crypto.randomUUID() on first run, saved to storage.local under 'uid' with no expiration.

The write wasn't directly observed, but the same UUID appeared in all six reports across worker-reload gaps.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You install the extension, or Chrome updates it to a new version.

This fires the extension's install/update handler.

The extension did this

The background service worker checks chrome.storage.local for a saved ID; if none exists, it generates one and stores it permanently.

No expiration or rotation is set on the stored value.

02EvidenceSTORAGE DUMP
What's stored on your device

Stores a single persistent identifier that never expires and is reused for the life of the install, generated once via crypto.randomUUID().

Locationchrome.storage.local key 'uid'
Contents (JSON)
{
  "uid": "b8305e74-fbd3-4a11-9b44-c4a7d9eac614"
}
03EvidenceCODE COMPARE
The code that does this

UUID generation and storage on install/update

What it actually does
const setToSessionStorage = async (e, t) => {
  await chrome.storage.session.set({
    [e]: t
  })
}, getFromSessionStorage = async e => (await chrome.storage.session.get(e))[e], getFromChromeLocalStorage = async e => (await chrome.storage.local.get(e))[e], setToChromeLocalStorage = async (e, t) => {
  let a = {};
  a[e] = t, await chrome.storage.local.set(a)
}, generateUserId = () => "undefined" != typeof crypto && crypto.randomUUID ? crypto.randomUUID() : "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (function(e) {
  const t = 16 * Math.random() | 0;
  return ("x" === e ? t : 3 & t | 8).toString(16)
})), initializeUserId = async () => {
  try {
    const e = await getFromChromeLocalStorage("uid");
    if (!e) {
      const e = generateUserId();
      return await setToChromeLocalStorage("uid", e), e
    }
    return e
  } catch (e) {
    return console.error("Error initializing user ID:", e), null
  }
}, tabInfo = async e => {
  let t = await getFromSessionStorage("ferers") || {};
  return e in t || (t[e] = {}), t[e]
}, handleRuntimeError = () => {
  let e = chrome.runtime.lastError;
  e && console.log(e)
}, isValidPage = e => null != e && e.startsWith("http"), postData = async (e, t = {}) => {
  try {
    const a = JSON.stringify(t),
      r = btoa(a),
      o = await fetch(e, {
        method: "POST",
        credentials: "include",
        headers: {
          "Content-Type": "text/plain"
        },
        body: r
      });
    return await o.json()
  } catch (e) {}
};
chrome.tabs.onUpdated.addListener((async (e, t, a) => {
  if ("complete" === t.status) {
    handleRuntimeError();
    const t = await getFromChromeLocalStorage("uid"),
      r = await tabInfo(e);
    let o = r?.url;
    if (isValidPage(a.url) && a.url !== o) {
      let r = {
        uri: a.url,
        uid: t,
        docref: o
      };
      await postData("https://pipextension.com/api/reports", r);
      let s = await getFromSessionStorage("ferers") || {};
      s[e] = {
        url: a.url
      }, await setToSessionStorage("ferers", s)
    }
  }
})), chrome.runtime.onInstalled.addListener((async e => {
  "install" === e.reason ? (await initializeUserId(), chrome.tabs.create({
    url: "https://pipextension.com/#how-it-works"
  })) : "update" === e.reason && await initializeUserId()
})), chrome.action.onClicked.addListener((e => {
  chrome.scripting.executeScript({
    target: {
      tabId: e.id
    },
    files: ["pip.js"]
  }, (() => {
    chrome.tabs.sendMessage(e.id, {
      message: "togglePictureInPicture"
    })
  }))
})), chrome.runtime.onMessage.addListener((async function(e) {
  "open" === e.state ? chrome.action.setIcon({
    path: "pip-dark128.png"
  }) : "close" === e.state && chrome.action.setIcon({
    path: "pip-light128.png"
  }), "disabled" === e.state && chrome.action.setIcon({
    path: "pip-light128.png"
  })
}));
04EvidenceTHIRD PARTY LIST
Third-party destinations
  • pipextension.com

    Receives this persistent ID in every browsing-history report the extension sends on page navigation.

What it can do

Permissions this extension asks for, as declared in version 1.2. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.

  • Read and change your data on every site you visit

    <all_urls>

  • Store data in your browser

    storage

  • Act on the current tab, but only after you click the extension

    activeTab

  • Run its own code inside the pages you visit

    scripting

Updated 30 September 2026pmdjjeplkafhkdjebfaoaljknbmilfgo