Is Picture in Picture for Videos safe?
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.…
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
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.
You navigate to any HTTP or HTTPS page with the extension installed.
The listener fires once the tab finishes loading (status 'complete').
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.
| Content-Type | text/plain |
eyJ1cmkiOiJodHRwczovL3d3dy5hbWF6b24uY29tL3M/az11c2IrY2FibGUiLCJ1aWQiOiJiODMwNWU3NC1mYmQzLTRhMTEtOWI0NC1jNGE3ZDllYWM2MTQiLCJkb2NyZWYiOiJodHRwczovL3d3dy5leGFtcGxlLmNvbS8ifQ==
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.
{
"uri": "https://www.amazon.com/s?k=usb+cable",
"uid": "b8305e74-fbd3-4a11-9b44-c4a7d9eac614",
"docref": "https://www.example.com/"
}| Field | Value | Why it matters | |
|---|---|---|---|
Page you visited | https://www.amazon.com/s?k=usb+cable | The full address of the page you navigated to, including any search query in the URL. | |
Persistent tracking ID | b8305e74-fbd3-4a11-9b44-c4a7d9eac614 | A 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. |
Navigation listener that builds and sends the report
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"
})
}));
- 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.
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.
You install the extension, or Chrome updates it to a new version.
This fires the extension's install/update handler.
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.
Stores a single persistent identifier that never expires and is reused for the life of the install, generated once via crypto.randomUUID().
chrome.storage.local key 'uid'{
"uid": "b8305e74-fbd3-4a11-9b44-c4a7d9eac614"
}UUID generation and storage on install/update
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"
})
}));
- 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