Is Force Paster safe?
Force Paster sends paste activity, active hostnames, and field tags to a Cloud Functions analytics proxy.
When paste handling is enabled, Force Paster listens for paste events on HTTP and HTTPS pages and records the active site hostname and the type of field being pasted into. After each paste, it sends an analytics event with that hostname, field tag, paste counters, and persistent extension identifiers to its Cloud Functions proxy.
Who publishes itN/A - no other listings under this identity, 4 shared hostnames
N/A - no other listings under this identity, 4 shared hostnames
What this publisher told the store about itself, and the other listings that told it the same thing.
Shared hosts - 4 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
Paste events transmit site and field context
Force Paster sends a paste analytics event whenever a paste occurs on an HTTP/HTTPS page.
It records the site hostname, field tag, paste counters, a client ID, a session ID, and a bearer token, then posts to the Cloud Functions proxy.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You paste text into a field after turning Force Paster on.
The extension records the active site and field type, then sends a paste analytics event with persistent identifiers.
- Active site hostnameexample.com (illustrative)
Shows which website was active when you pasted into the page.
- Focused field taginput
Shows the type of page element that received the paste action.
- Paste count1
Shows how many paste actions the extension has counted in this browser profile.
- Paste-count bucket1
Groups your paste activity into a range for analytics reporting.
- Persistent client identifier2654312809.1752334593 (illustrative)
Lets the analytics endpoint connect events from the same browser profile over time.
- Session identifier1752334593012 (illustrative)
Links paste events that happen during the same active browser session.
- Install bearer tokenBearer <redacted>
Identifies the extension installation when the analytics request is accepted.
- Content-Type
- application/json
- Authorization
- Bearer <redacted>
- X-Extension-Id
- bdnpdmmgbnaahbeeoickafhpcidhgdhc
- X-Extension-App
- forcepaster
Paste event collection and analytics POST path
Content script paste listener
content.jsdocument.addEventListener('paste', event => { if (!forcePasterSettings.isPasteEnabled) return; if (_syntheticPaste) return; const currEle = document.activeElement; const isField = isInputOrTextarea(currEle); const isEditable = isContentEditable(currEle); if (!isField && !isEditable) return; // Seize the event — stop all other listeners (including the site's) from // seeing it, then cancel the browser's default paste behaviour. event.stopImmediatePropagation(); event.preventDefault(); const clipboardData = event.clipboardData || window.clipboardData || event.originalEvent?.clipboardData; const pastedText = clipboardData.getData('Text'); chrome.runtime.sendMessage({ type: "onpastestart", on: currEle.tagName.toLowerCase() }); if (isField) { pasteIntoInputField(currEle, pastedText); } else { pasteIntoContentEditable(currEle, pastedText, clipboardData); } chrome.runtime.sendMessage({ type: "onpastecomplete" }, response => { if (response?.showRatingPrompt) { showRatingToast(); } });}, true /* capture */);Service worker message handler
service_worker.jswebext.runtime.onMessage.addListener(async (request, sender, sendResponse) => { await settingsReady; const { type, on } = request; if (type === "themechange") { const icon_paths = { "16": `icons/${request.mode}/icon16.png`, "32": `icons/${request.mode}/icon32.png`, "48": `icons/${request.mode}/icon48.png`, "128": `icons/${request.mode}/icon128.png` }; webext.action.setIcon({ path: icon_paths }); } else if (type === "onpastestart") { webext.tabs.query({ active: true, currentWindow: true }, tabs => { saveAndApplyExtensionDetails({ lastDomain: (new URL(tabs[0].url)).hostname, lastTag: on, }); }); } else if (type === "onpastecomplete") { saveAndApplyExtensionDetails({ pasteCount: forcePasterSettings.pasteCount + 1 }); const totalPastes = forcePasterSettings.pasteCount; const showRatingPrompt = shouldShowRatingPrompt(forcePasterSettings); // Respond immediately so the message channel doesn't close before analytics finishes. sendResponse({ totalPastes, showRatingPrompt, }); sendProxyEvent("fp_paste", { tag: forcePasterSettings.lastTag ?? null, domain: forcePasterSettings.lastDomain ?? null, source: "paste_event", ...withPasteCountAnalytics(totalPastes), }).catch(e => console.warn("analytics fp_paste failed", e)); } else if (type === "optionsopen") { sendProxyEvent("fp_options_open", { source: request.source ?? "manual", ...withPasteCountAnalytics(forcePasterSettings.pasteCount), }).catch(() => {}); sendResponse({ ok: true }); } else if (type === "optionsclick") { sendProxyEvent("fp_options_click", { item: request.item, ...withPasteCountAnalytics(forcePasterSettings.pasteCount), }).catch(() => {}); sendResponse({ ok: true }); } else if (type === "ratingresponse") { let ratingState, snoozeGap; if (request.choice === "later") { const currentGap = forcePasterSettings.snoozeGap ?? RATING_SNOOZE_INITIAL; const nextGap = Math.min(Math.round(currentGap * RATING_SNOOZE_MULTIPLIER), RATING_SNOOZE_CAP); ratingState = forcePasterSettings.pasteCount + nextGap; snoozeGap = nextGap; } else { ratingState = "done"; } saveAndApplyExtensionDetails({ ratingState, ...(snoozeGap !== undefined && { snoozeGap }) }); if (request.choice === "rate") { webext.tabs.create({ url: RATING_STORE_URL }); } sendResponse({ ok: true }); sendProxyEvent("fp_rating_prompt", { choice: request.choice, ...withPasteCountAnalytics(forcePasterSettings.pasteCount), }).catch(e => console.warn("analytics fp_rating_prompt failed", e)); } else if (type === "setenabled") { saveAndApplyExtensionDetails({ isPasteEnabled: request.enabled, clickCount: forcePasterSettings.clickCount + 1, }); try { await sendProxyEvent("fp_toggle", { enabled: request.enabled, source: "options_page", ...withPasteCountAnalytics(forcePasterSettings.pasteCount), }); } catch (e) { console.warn("analytics fp_toggle failed", e); } sendResponse({ ok: true }); } return true;});Paste-count analytics helper
service_worker.jsfunction getPasteCountBucket(pasteCount) { if (!Number.isFinite(pasteCount) || pasteCount <= 0) return "0"; if (pasteCount <= 5) return String(pasteCount); if (pasteCount <= 50) { const start = Math.floor((pasteCount - 1) / 5) * 5 + 1; const end = start + 4; return `${start}-${end}`; } if (pasteCount <= 1000) { const start = Math.floor((pasteCount - 1) / 50) * 50 + 1; const end = start + 49; return `${start}-${end}`; } if (pasteCount <= 5000) { const start = Math.floor((pasteCount - 1) / 500) * 500 + 1; const end = start + 499; return `${start}-${end}`; } return "5000+";}function withPasteCountAnalytics(pasteCount) { return { paste_count: pasteCount, paste_count_bucket: getPasteCountBucket(pasteCount), };}Analytics identifier creation and POST
analytics.jsasync function getOrCreateClientId() { const result = await chrome.storage.local.get(CLIENT_ID_STORAGE_KEY); let clientId = result[CLIENT_ID_STORAGE_KEY]; if (!clientId || !VALID_CLIENT_ID_RE.test(clientId)) { const unixSeconds = Math.floor(Date.now() / 1000); clientId = `${randomUint32()}.${unixSeconds}`; await chrome.storage.local.set({ [CLIENT_ID_STORAGE_KEY]: clientId }); } return clientId;}async function getOrCreateSessionId() { const now = Date.now(); const result = await chrome.storage.session.get(SESSION_STORAGE_KEY); let session = result[SESSION_STORAGE_KEY]; if (session?.timestamp) { const durationMin = (now - Number(session.timestamp)) / 60000; if (durationMin <= SESSION_EXPIRATION_MIN) { session.timestamp = String(now); await chrome.storage.session.set({ [SESSION_STORAGE_KEY]: session }); return session.session_id; } } session = { session_id: String(now), timestamp: String(now) }; await chrome.storage.session.set({ [SESSION_STORAGE_KEY]: session }); return session.session_id;}async function registerInstallToken() { const res = await fetch(`${PROXY_BASE_URL}/v1/register`, { method: "POST", headers: { "Content-Type": "application/json", "X-Extension-App": "forcepaster", "X-Extension-Id": chrome.runtime.id }, body: JSON.stringify({}) }); if (!res.ok) { const txt = await res.text(); throw new Error(`register failed: ${res.status} ${txt}`); } const data = await res.json(); if (!data?.token) throw new Error("register failed: missing token"); await chrome.storage.local.set({ [TOKEN_STORAGE_KEY]: data.token }); return data.token;}async function getOrRegisterToken(force = false) { if (!force) { const result = await chrome.storage.local.get(TOKEN_STORAGE_KEY); const token = result[TOKEN_STORAGE_KEY]; if (token) return token; } return registerInstallToken();}export async function sendProxyEvent(eventName, params = {}, opts = {}) { const client_id = await getOrCreateClientId(); const session_id = await getOrCreateSessionId(); const token = await getOrRegisterToken(false); const debug = opts.debug ?? false; const payload = { client_id, events: [ { name: eventName, params: { session_id, engagement_time_msec: DEFAULT_ENGAGEMENT_TIME_MS, ext_version: chrome.runtime.getManifest().version, ...params, } } ] }; if (opts.userProperties) { payload.user_properties = toUserProperties(opts.userProperties); } const url = debug ? `${PROXY_BASE_URL}/v1/collect?debug_view=1` : `${PROXY_BASE_URL}/v1/collect`; async function doSend(bearerToken) { return fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${bearerToken}`, "X-Extension-App": "forcepaster", "X-Extension-Id": chrome.runtime.id }, body: JSON.stringify(payload) }); } let res = await doSend(token); if (res.status === 401) { // Token missing/invalid. Re-register once and retry once. const newToken = await getOrRegisterToken(true); res = await doSend(newToken); } if (!res.ok && !debug) { const txt = await res.text(); throw new Error(`collect failed: ${res.status} ${txt}`); } return res;}What it can do
Permissions this extension asks for, as declared in version 2.5.4. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to. The current listed version is 2.5.8, which we have not unpacked yet.
Read and change your data on every site you visit
<all_urls>
Store data in your browser
storage
See the address and title of every tab you have open
tabs
Add items to the right-click menu
contextMenus
Where it sends data
Destinations our analysis observed Force Paster 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.
- asia-south1-extensions-analytics-prod.cloudfunctions.net
Force Paster sends data to asia-south1-extensions-analytics-prod.cloudfunctions.net. 2 other extensions we have analysed send data here.