Is Zapier Agents safe?

Medium risk

Zapier Agents is medium risk. Zapier Agents injects an active-tab script during the browser-context flow, returning page text, title, and URL to the Zapier Agents page. Testing did not observe this without login, but the code path is present in the shipped extension.…

Zapierv1.4.31Chrome Web Store
45Risk
Who publishes it

Zapier, Inc. - 1 other listing from the same operator, none carrying a finding

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
Zapier
Declared legal entity
Zapier, Inc.
Registered address
548 Market St #62411, San Francisco, CA 94104-5401, US
Registered contact
Renee Law

Same store account

1 other listing published from this account, 40k+ users between them, none of them carrying a finding.

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

Publishers can request a review.

Findings

Active-tab page text is relayed to Zapier Agents

HighunwantedCWE-200Dynamic sandbox

Zapier Agents injects an active-tab script during the browser-context flow, returning page text, title, and URL to the Zapier Agents page.

What actually happens

You did this

You use the Zapier Agents side panel while browsing in an active tab.

The flow depends on Zapier authentication and the side panel being open.

The extension did this

The extension reads the active tab's visible page text, title, and URL and returns them to the Zapier Agents page.

The service worker sends getPageContent to active-tab-script.js and forwards the response back through the content-script message path.

Fields returned from the active tab

  • Active tab URL
    https://example.com/account/settings

    Shows the exact page address open in your active browser tab.

  • Page title
    Account Settings

    Identifies the page or document visible in the browser tab.

  • Visible page text
    Project notes and account details visible on the page

    Can include readable content from the active page, including private document, workspace, or account text.

  • Google document text
    Quarterly planning notes from a Google Doc

    When the active tab is a Google Doc, the extension can request document content through the Google Docs API.

  • Google spreadsheet values
    This text represents the values in the rows of a spreadsheet: [["Account","Owner"]]

    When the active tab is a Google Sheet, the extension can request spreadsheet grid values through the Google Sheets API.

Captured request

GEThttps://agents.zapier.com/chrome-extension

Loaded as the Zapier Agents side-panel iframe; no request body is present for this navigation.

The code that does this

The service worker injects the active-tab reader and requests page content

Readable version

Deobfuscated service-worker.js getBrowserContext handler

service-worker.js
chrome.runtime.onMessage.addListener(((t, e, n) => {if ("getBrowserContext" === t) return async function() {  const t = await vr();  return new Promise(((e, n) => {    t.id ? chrome.tabs.sendMessage(t.id, "getPageContent", (function(r) {      r ? e(r) : (t.url || e(void 0), je(`Unable to get page data from tab: ${t.url} (Last error: ${chrome.runtime.lastError?.message})`), n(new Error(`Unable to get page data from tab: ${t.url}`)))    })) : n(new Error("No current tab"))  }))}().then((t => {  n(t)})).catch((() => {  n("")})), !0}))

Deobfuscated service-worker.js active-tab injection hooks

service-worker.js
chrome.tabs.onActivated.addListener((async ({tabId: t}) => {if (t) {  try {    await chrome.scripting.executeScript({      target: {        tabId: t      },      files: ["active-tab-script.js"]    })  } catch {}  chrome.tabs.query({    active: !0,    currentWindow: !0  }, (async ([t]) => {    Sr(t.url?.includes("linkedin.com") ?? !1)  }))}})), chrome.tabs.onUpdated.addListener(((t, e) => {(e.url || e.status) && chrome.tabs.query({  active: !0,  currentWindow: !0}, (async ([e]) => {  try {    await chrome.scripting.executeScript({      target: {        tabId: t      },      files: ["active-tab-script.js"]    })  } catch {}  Sr(e.url?.includes("linkedin.com") ?? !1)}))}))

The code that does this

The active-tab script returns page text, title, and URL

Readable version
chrome.runtime.onMessage.addListener((function(t, e, n) {  return "getPageContent" === t && (A({    category: "chrome-extension",    message: `getPageContent from ${window.location.href}`,    level: "info"  }), window.location.href.includes("docs.google.com/document") ? (A({    category: "chrome-extension",    message: "getPageContent from Google Doc",    level: "info"  }), chrome.runtime.sendMessage("getAuthToken", (function(t) {    if (t) {      const e = new RegExp(/https:\/\/docs.google.com\/document\/d\/(.*)\//).exec(window.location.href),        s = e?.[1];      if (s) {        const e = {          method: "GET",          headers: {            Authorization: `Bearer ${t}`,            "Content-Type": "application/json"          }        };        try {          fetch(`https://docs.googleapis.com/v1/documents/${s}`, e).then((t => t.json())).then((function(t) {            const e = t.body?.content?.map((t => t.paragraph?.elements)).flat().filter((t => Boolean(t))).map((t => t?.textRun)),              s = e?.map((t => t?.content)).join("") ?? "";            n({              source: "Google Docs",              text: s,              title: t.title,              url: window.location.href            })          }))        } catch (t) {          j(`Fetch failed for for https://docs.googleapis.com/v1/documents/${s}`)        }      } else n({        source: "Google Docs",        text: "Unable to get the content of this document",        title: document.title,        url: window.location.href      }), j(`No documentId found for ${window.location.href}`)    } else n({      source: "Google Docs",      text: "Unable to get the content of this document",      title: document.title,      url: window.location.href    }), j(`No auth token found for ${window.location.href}`)  }))) : window.location.href.includes("docs.google.com/spreadsheets") ? (A({    category: "chrome-extension",    message: "getPageContent from Google Sheet",    level: "info"  }), chrome.runtime.sendMessage("getAuthToken", (function(t) {    if (t) {      const e = new RegExp(/https:\/\/docs.google.com\/spreadsheets\/d\/(.*)\/edit\?gid=(.*)#/).exec(window.location.href),        s = e?.[1],        o = e?.[2];      if (s) {        const e = {          method: "GET",          headers: {            Authorization: `Bearer ${t}`,            "Content-Type": "application/json"          }        };        try {          fetch(`https://sheets.googleapis.com/v4/spreadsheets/${s}?includeGridData=true`, e).then((t => t.json())).then((function(t) {            const e = t.sheets?.find((t => t.properties?.sheetId === Number(o))),              s = e?.data?.map((t => t.rowData?.map((t => t.values?.map((t => t.formattedValue)))))).flat(1);            n({              source: "Google Sheets",              text: `This text represents the values in the rows of a spreadsheet: ${JSON.stringify(s)}`,              title: t.properties?.title ?? document.title,              url: window.location.href            })          }))        } catch (t) {          j(`Fetch failed for for https://sheets.googleapis.com/v4/spreadsheets/${s}`)        }      } else n({        source: "Google Sheets",        text: "Unable to get the content of this spreadsheet",        title: document.title,        url: window.location.href      }), j(`No spreadsheetId found for ${window.location.href}`)    } else n({      source: "Google Sheets",      text: "Unable to get the content of this spreadsheet",      title: document.title,      url: window.location.href    }), j(`No auth token found for ${window.location.href}`)  }))) : window.location.href.includes("linkedin.com") ? (A({    category: "chrome-extension",    message: "getPageContent from LinkedIn",    level: "info"  }), n({})) : (A({    category: "chrome-extension",    message: "getPageContent from standard webpage",    level: "info"  }), n({    text: document.documentElement.innerText,    title: document.title,    url: window.location.href  }))), !0}))

The code that does this

The side panel embeds the remote Zapier Agents page

Readable version
<html><body><head><style>html, body {        margin: 0;        padding: 0;        height: 100%;      }      iframe {        position: fixed;        inset: 0;        width: 100%;        height: 100%;        border: 0;      }</style><script defer="defer" src="sidepanel.js"></script></head><iframe src="https://agents.zapier.com/chrome-extension" id="central" allow="clipboard-write"></iframe></body></html>

Remote services named in the active-tab content path

    • agents.zapier.com

    Zapier Agents iframe and page context that receives browser-context responses.

    • docs.googleapis.com

    Google Docs API endpoint queried when the active tab is a Google Doc and an OAuth token is available.

    • sheets.googleapis.com

    Google Sheets API endpoint queried when the active tab is a Google Sheet and an OAuth token is available.

Visible tab screenshots relayed to Zapier Agents on request

MediumunwantedCWE-200Dynamic sandbox

The extension can capture the visible browser tab as a JPEG when its Zapier Agents page requests a screenshot.

What actually happens

You did this

A logged-in Zapier Agents side panel requests a browser screenshot.

The extension did this

The extension captures the visible tab as a JPEG and relays the screenshot response to Zapier Agents.

Captured request

POSThttps://agents.zapier.com

Fields in the request

  • Visible tab information
    Screenshot of the active tab (illustrative)

    The request can include what is currently visible in your active browser tab, including page content shown on screen.

Observation

Dynamic analysis confirmed the screenshot capture path in the extension code. Earlier testing could not trigger the behavior without Zapier authentication and an open side panel.

Updated 15 October 2026jfcmjbboehfdmgbhheahjlnoimbgfdbn