Is Force Paster safe?

Medium risk

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.

Pratyush Vashishtv2.5.8Chrome Web Store
48Risk
Who publishes it

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.

Publisher
Pratyush Vashisht
Declared legal entity
N/A
Registered address
Herengracht 597, Amsterdam 1017 CE, NL
Registered contact
Pratyush Vashisht

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.

watermark.page
Also called by 1 other listing: Force Paster
asia-south1-extensions-analytics-prod.cloudfunctions.net
Also called by 2 other listings, including Force Paster
vashis.ht
Also called by 2 other listings, including Force Paster
pratyushvashisht.com
Also called by 3 other listings, including Force Paster

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

You paste text into a field after turning Force Paster on.

The extension did this

The extension records the active site and field type, then sends a paste analytics event with persistent identifiers.

Fields included in the paste analytics path
  • Active site hostname
    example.com (illustrative)

    Shows which website was active when you pasted into the page.

  • Focused field tag
    input

    Shows the type of page element that received the paste action.

  • Paste count
    1

    Shows how many paste actions the extension has counted in this browser profile.

  • Paste-count bucket
    1

    Groups your paste activity into a range for analytics reporting.

  • Persistent client identifier
    2654312809.1752334593 (illustrative)

    Lets the analytics endpoint connect events from the same browser profile over time.

  • Session identifier
    1752334593012 (illustrative)

    Links paste events that happen during the same active browser session.

  • Install bearer token
    Bearer <redacted>

    Identifies the extension installation when the analytics request is accepted.

Captured request
POSThttps://asia-south1-extensions-analytics-prod.cloudfunctions.net/extensions-ga-proxy/v1/collect
Headers
Content-Type
application/json
Authorization
Bearer <redacted>
X-Extension-Id
bdnpdmmgbnaahbeeoickafhpcidhgdhc
X-Extension-App
forcepaster
The code that does this

Paste event collection and analytics POST path

Readable version

Content script paste listener

content.js
document.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.js
webext.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.js
function 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.js
async 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.

Updated 30 September 2026bdnpdmmgbnaahbeeoickafhpcidhgdhc