Is Browse AI: Fast Web Scraping & Monitoring safe?

High risk

This extension helps users create no-code web scraping and monitoring automations from websites.

It provides a point-and-click workflow for training robots to extract web data, download results as spreadsheets, sync with Google Sheets, and monitor pages for changes on a schedule. The service can also send data to other tools through Zapier, REST API, and webhooks, and it collects anonymous usage data that can be disabled in Settings.

Browse AI Inc.v2.2.3Chrome Web Store
75Risk

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

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

Browse AI sends recording cookies to its API

An authenticated Browse AI session can collect cookies for the recorded site and domains requested in the recorder tab, then pass them into CreateRobot's GraphQL mutation.

Unauthenticated analysis triggered no cookie or API calls.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You start a Browse AI recording session after signing in.

The cookie-reading branch requires recording permissions and the session-cookie recording option.

The extension did this

The extension reads cookies for the recorded site and request origins, then includes them when the robot is saved.

Unauthenticated test runs did not reach this path, which matches the login and recording preconditions in the code.

02EvidenceFIELD TABLE
Cookie object fields passed into the GraphQL mutation
FieldValueWhy it matters
Cookie name
sessionid (illustrative)This identifies which site cookie is being copied from your browser.
Cookie value
7f4a9c2e51b06d8a (illustrative)This can carry signed-in session state or other account-specific data for the recorded site.
Cookie domain
.example.com (illustrative)This ties the copied cookie to the website or subdomain where it applies.
Cookie path
/account (illustrative)This narrows where the copied cookie is valid within the site.
Expiration
2026-08-15T12:00:00Z (illustrative)This shows whether the copied cookie may remain useful beyond the current browser session.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://internal-api.browse.ai/graphql
04EvidenceCODE COMPARE
The code that does this

Service-worker paths that gather cookies and submit them with CreateRobot

What it actually does
Cookie reader used by the recorderservice_worker.js
const getDomainCookies = ({
  domain,
  name,
  exclude = []
}) => new Promise(
  (resolve) => chrome.cookies.getAll({ domain, name }, (cookies) => {
    resolve(
      cookies.filter((cookie) => cookie.expirationDate && cookie.expirationDate > 0).filter(
        (cookie) => exclude.findIndex(
          (excludedCookie) => excludedCookie.domain === cookie.domain && excludedCookie.name === cookie.name
        ) === -1
      )
    );
  })
);
Recording startup collects cookies for the origin domainservice_worker.js
const startRecording = (params) => new Promise(async (resolve, reject) => {
  const {
    url,
    rerecording = false,
    shouldRecordSessionCookies,
    teamName
  } = params;
  void log$4("info", "Start recording", { rerecording });
  if (!url) {
    reject(new Error("Origin URL cannot be blank."));
    return;
  }
  if (getEnv$1() === "dev") {
    await resetRecordingState(params);
    const windowName = 999888777;
    window.open(
      "about:blank",
      windowName.toString(),
      ["menubar=0"].join(",")
    );
    await (rerecording ? playingBackState : recordingState$1).set(true);
    await recorderTabState$3.set({ id: windowName, url, teamName });
    resolve();
  } else {
    const grantedPermissions = await allPermissionsGranted();
    await resetRecordingState(params);
    const recorderTab = await recorderTabState$3.get();
    if (recorderTab) {
      reject(
        new Error(
          "Cannot start recording a new robot. There is already a recording in progress."
        )
      );
      return;
    }
    if (!grantedPermissions) {
      reject(new Error("Recording permissions were not granted."));
      return;
    }
    if (shouldRecordSessionCookies) {
      const origin = getURLOrigin(url);
      if (origin) {
        const originCookies = await getDomainCookies({
          domain: origin.domain
        });
        await recordedCookiesState$1.set(originCookies);
      }
    } else {
      const incognito = await hasIncognitoAccess();
      if (!incognito) {
        reject(
          new Error(
            `Incognito access is required for recording from a blank slate.`
          )
        );
        return;
      }
    }
    const {
      window: window2,
      createData,
      windowCreateSecondParam,
      availableScreenWidth,
      availableScreenHeight
    } = await createBlankPopUpWindow({
      maxHeight: params.maxHeight,
      maxWidth: params.maxWidth,
      incognito: !shouldRecordSessionCookies
    });
    if (!window2 || !window2.tabs || !window2.tabs[0] || !window2.tabs[0].id) {
      void log$4(
        "error",
        "startRecording: New tab was not created or does not have an ID.",
        {
          window: window2,
          createData,
          windowCreateSecondParam,
          availableScreenWidth,
          availableScreenHeight,
          runtimeLastError: chrome.runtime.lastError
        }
      );
      reject(
        new Error(
          `startRecording: New tab was not created or does not have an ID.`
        )
      );
      return;
    }
    await (rerecording ? playingBackState : recordingState$1).set(true);
    await recorderTabState$3.set({ id: window2.tabs[0].id, url, teamName });
    resolve();
  }
});
Request listener adds cookies for each recorder-tab originservice_worker.js
const recordedCookiesState = createPersistedState(
  "recordedCookies",
  []
);
const recordCookiesBeforeRequest = async (recorderTab) => {
  const recordedCookies = await recordedCookiesState.get();
  const onBeforeSendHeadersHandler = (details) => {
    if (details.url) {
      const origin = getURLOrigin(details.url);
      if (origin) {
        void getDomainCookies({
          domain: origin.domain,
          exclude: recordedCookies
        }).then(async (cookies) => {
          recordedCookies.push(...cookies);
          await recordedCookiesState.set(recordedCookies);
        });
      }
    }
  };
  if (recorderTab) {
    chrome.webRequest.onBeforeSendHeaders.addListener(
      onBeforeSendHeadersHandler,
      { tabId: recorderTab.id, urls: ["http://*/*", "https://*/*"] }
    );
  }
  const cleanUp = () => new Promise((resolve) => {
    chrome.permissions.getAll(
      ({ permissions }) => {
        if (permissions && permissions.includes("webRequest") && chrome.webRequest.onBeforeSendHeaders.hasListener(
          onBeforeSendHeadersHandler
        )) {
          chrome.webRequest.onBeforeSendHeaders.removeListener(
            onBeforeSendHeadersHandler
          );
        }
        void recordedCookiesState.set([]).then(resolve);
      }
    );
  });
  return cleanUp;
};
Origin probe collects cookies from all requested domainsservice_worker.js
async function recordCookiesOfOriginUrl({
  originUrl
}) {
  const granted = await chrome.permissions.request({
    permissions: ["cookies"],
    origins: ["http://*/*", "https://*/*"]
  });
  if (!granted) {
    throw new Error("Recording permissions were not granted.");
  }
  return new Promise(async (resolve) => {
    const cookies = {};
    const rawCookies = [];
    const newOpenedWindow = await chrome.windows.create({
      url: originUrl,
      type: "popup",
      width: 1,
      height: 1,
      focused: false,
      top: 0,
      left: 0
    });
    if (!newOpenedWindow || !newOpenedWindow.id || !newOpenedWindow.tabs) {
      throw new Error("Failed to open a new window");
    }
    const newTabId = newOpenedWindow.tabs[0].id;
    const newWindowId = newOpenedWindow.id;
    await recorderTabState$2.set({
      id: newWindowId,
      url: originUrl,
      teamName: "none"
    });
    function addCookie(domainCookies) {
      rawCookies.push(...domainCookies);
      domainCookies.forEach((cookie) => {
        cookies[cookie.domain] = cookies[cookie.domain] || {};
        cookies[cookie.domain][cookie.path] = cookies[cookie.domain][cookie.path] || {};
        cookies[cookie.domain][cookie.path][cookie.name] = cookie;
      });
    }
    function grabCookieFromOriginUrl(req) {
      const { url } = req;
      const domain = new URL(url).hostname;
      void chrome.cookies.getAll({ domain }).then((domainCookies) => {
        addCookie(domainCookies);
      });
    }
    chrome.webRequest.onBeforeRequest.addListener(grabCookieFromOriginUrl, {
      windowId: newOpenedWindow.id,
      urls: ["<all_urls>"]
    });
    chrome.tabs.onUpdated.addListener(async (tabId, changeInfo) => {
      if (tabId === newTabId && changeInfo.status === "complete") {
        chrome.webRequest.onBeforeRequest.removeListener(
          grabCookieFromOriginUrl
        );
        const url = new URL(originUrl);
        const fullHostname = url.hostname;
        const parts = fullHostname.split(".");
        const baseDomain = parts.slice(-2).join(".");
        if (fullHostname) {
          const fullHostnameCookies = await chrome.cookies.getAll({
            domain: fullHostname
          });
          addCookie(fullHostnameCookies);
        }
        if (baseDomain && baseDomain !== fullHostname) {
          const baseDomainCookies = await chrome.cookies.getAll({
            domain: baseDomain
          });
          addCookie(baseDomainCookies);
        }
        void chrome.windows.remove(newWindowId);
        resolve(normalizeCookies(cookies));
      }
    });
  });
}
CreateRobot mutation includes the cookies variableservice_worker.js
const createRobot = async (variables) => {
  const result = await getAPIClient().mutate({
    mutation: gql`
      mutation CreateRobot(
        $chromeVersion: String!
        $extensionVersion: String!
        $purpose: RobotPurpose!
        $cookies: JSON!
        $recordedSessionCookies: Boolean!
        $teamId: String!
      ) {
        createRobot(
          chromeVersion: $chromeVersion
          extensionVersion: $extensionVersion
          purpose: $purpose
          cookies: $cookies
          recordedSessionCookies: $recordedSessionCookies
          teamId: $teamId
        ) {
          id
          uploadingSteps {
            id
            rawStepsURL
          }
        }
      }
    `,
    variables: {
      ...variables,
      chromeVersion: getBrowserVersion() || "",
      extensionVersion: getExtensionVersion()
    }
  });
  const robotId = result.data.createRobot.id;
  const robotVersionId = result.data.createRobot.uploadingSteps.id;
  const rawStepsURL = result.data.createRobot.uploadingSteps.rawStepsURL;
  await createdRobotState$1.set({
    robotId,
    robotVersionId,
    rawStepsURL
  });
  await finishRobotRecording({
    robotId,
    robotVersionId,
    rawStepsURL
  });
  await createdRobotState$1.set(null);
  return { robotId, robotVersionId, rawStepsURL };
};
05EvidenceTHIRD PARTY LIST
Destination that receives the recording payload
  • internal-api.browse.ai

    Browse AI GraphQL API endpoint used when the extension saves a recorded robot with cookie JSON.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

Browse AI uploads recorded page and DOM step details

Browse AI recording captures interaction steps: page URL, title, DOM targets, page metadata, viewport size, scroll position, user agent.

The worker uploads steps via Browse AI's GraphQL API.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You record a Browse AI workflow on a web page.

The recording feature listens for page events while the recorder is active.

The extension did this

The extension stores detailed interaction steps and uploads them for the robot workflow.

Those steps include page context and DOM selectors tied to the recorded interaction.

02EvidenceFIELD TABLE
Data fields stored in recorded steps
FieldValueWhy it matters
Page URL
https://app.example.com/orders/1042?status=pendingShows the exact page where the recorded action happened.
Page title and metadata
Pending Orders - Example AdminAdds readable page context and selected metadata from the page.
DOM selectors
button[data-testid="approve-order"]Identifies the page elements involved in your recorded clicks and inputs.
Viewport and scroll position
1366x768, scrollTop 420Shows your browser viewport size and where the page was scrolled during the step.
User agent
Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0.0.0Can identify browser and operating-system details when the recorder includes it.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://internal-api.browse.ai/graphql
GraphQL uploadedRobotSteps mutation confirms that the recorded steps were uploaded.
04EvidenceCODE COMPARE
The code that does this

The content script builds step records with page and viewport context

What it actually does
Metadata collection helperdeobfuscated/content_script.js
    const wr = function() {
      for (var e = [], t = document.getElementsByTagName("meta"), n = document.getElementsByTagName("link"), r = 0; r < t.length; r++) {
        var i = t[r].getAttribute("name"),
          o = t[r].getAttribute("property"),
          a = t[r].getAttribute("content");
        a && ("msapplication-TileColor" === i || "msapplication-TileImage" === i || "application-name" === i || "description" === i || "robots" === i ? e.push({
          itemType: "meta",
          name: i,
          content: a
        }) : "og:site_name" !== o && "og:url" !== o && "og:image" !== o || e.push({
          itemType: "meta",
          property: o,
          content: a
        }))
      }
      for (var s = 0; s < n.length; s++) {
        var l = n[s].getAttribute("rel"),
          c = n[s].getAttribute("href"),
          u = n[s].getAttribute("sizes"),
          p = n[s].getAttribute("color"),
          d = n[s].getAttribute("title"),
          h = n[s].getAttribute("type"),
          m = n[s].getAttribute("crossOrigin");
        !c || "apple-touch-icon-precomposed" !== l && "icon" !== l && "shortcut icon" !== l && "mask-icon" !== l && "fluid-icon" !== l && "manifest" !== l && "canonical" !== l || e.push(yr(yr(yr(yr(yr({
          itemType: "link",
          rel: l,
          href: c
        }, u ? {
          sizes: u
        } : null), p ? {
          color: p
        } : null), d ? {
          title: d
        } : null), h ? {
          type: h
        } : null), m ? {
          crossOrigin: m
        } : null))
      }
      return e
    };
Step normalization helperdeobfuscated/content_script.js
    const Tr = function(e, t) {
      var n = e.event,
        r = e.targetSelectors,
        i = void 0 === r ? [] : r,
        o = e.containerSelectors,
        a = e.tabId,
        s = e.skipIfTargetNotFound,
        l = e.targetId,
        c = arguments.length > 2 && void 0 !== arguments[2] && arguments[2];
      return kr(kr({
        targetId: l,
        event: n,
        targetSelectors: i
      }, void 0 !== o ? {
        containerSelectors: o
      } : {}), {}, {
        time: Date.now(),
        page: kr({
          tabId: a,
          url: window.location.href,
          title: document.title,
          meta: wr()
        }, t),
        emulationOptions: {
          viewport: {
            width: Math.max(document.documentElement.clientWidth, window.innerWidth),
            height: Math.max(document.documentElement.clientHeight, window.innerHeight),
            deviceScaleFactor: Math.round(100 * window.devicePixelRatio) / 100
          },
          scrollLeft: window.pageXOffset || document.documentElement.scrollLeft,
          scrollTop: window.pageYOffset || document.documentElement.scrollTop,
          userAgent: c ? navigator.userAgent : null
        },
        skipIfTargetNotFound: s
      })
    };
Step persistence and callbackdeobfuscated/content_script.js
            for (;;) switch (e.prev = e.next) {
              case 0:
                return e.next = 2, gi.get();
              case 2:
                if (e.sent) {
                  e.next = 6;
                  break
                }
                return Di(), e.abrupt("return");
              case 6:
                return xi.push(t), e.next = 9, _i.set(xi);
              case 9:
                if (!pi) {
                  e.next = 12;
                  break
                }
                return e.next = 12, pi({
                  steps: [].concat(xi)
                });
              case 12:
              case "end":
                return e.stop()
            }
          }), e)
        })));
        return function(t) {
          return e.apply(this, arguments)
        }
      }(),
Recorder event listenersdeobfuscated/content_script.js
      }(), Mi || (Mi = !0, bi.forEach((function(e) {
        Hr(e) ? window.addEventListener(e, Ri, {
          capture: !0,
          once: "load" === e
        }) : document.addEventListener(e, Ri, {
          capture: !0
        })
      }))), _i.get().then(function() {
05EvidenceCODE COMPARE
The code that does this

The service worker uploads the recorded step list

What it actually does
PUT upload and GraphQL confirmationdeobfuscated/service_worker.js
const uploadRobotSteps = ({
  createdRobot: { robotVersionId, rawStepsURL },
  steps
}) => {
  return fetch(rawStepsURL, {
    method: "PUT",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(steps)
  }).then((res) => {
    if (res.status >= 400) {
      throw new Error(res.statusText);
    }
    return res;
  }).then(
    () => getAPIClient().mutate({
      mutation: gql`
            mutation uploadedRobotSteps($robotVersionId: String!) {
              uploadedRobotSteps(robotVersionId: $robotVersionId) {
                id
              }
            }
          `,
      variables: { robotVersionId }
    }).then((result) => {
      const robotVersionId2 = result.data.uploadedRobotSteps.id;
      return Boolean(robotVersionId2);
    })
  );
};
Finish recording uses stored stepsdeobfuscated/service_worker.js
const finishRobotRecording = async ({
  robotVersionId,
  rawStepsURL,
  robotId
}) => {
  const teamName = (await recorderTabState$5.get())?.teamName || void 0;
  const steps = await stepsState$2.get();
  const reloadSteps = await reloadStepsState$1.get();
  const mergedSteps = mergeStepsByTime(steps, reloadSteps);
  if (steps) {
    void log$6(
      "info",
      "Robot was created. Reset 'steps' and begin uploading the recording...",
      {}
    );
    await uploadRobotSteps({
      createdRobot: { robotVersionId, rawStepsURL },
      steps: mergedSteps
    });
    void log$6("info", "Robot recording upload was successful.");
06EvidenceTHIRD PARTY LIST
Browse AI endpoints involved in the upload flow
  • internal-api.browse.ai

    Browse AI GraphQL API receives the upload-complete mutation for recorded robot steps.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

Page URLs and referrers sent to Datadog during recording

Starting a Browse AI recording flow adds the current page URL, referrer, host, path, and scheme to its logging context, forwarded through the background logger to Datadog when anonymous data sharing stays enabled, the extension default.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You start a Browse AI recording flow.

The popup code can launch recording for the current tab when a recording purpose is selected.

The extension did this

The extension adds page URL and referrer fields to its logging context.

The background logger can forward that context to Datadog while anonymous data sharing is enabled.

02EvidenceFIELD TABLE
Fields added to the logging context
FieldValueWhy it matters
Current page URL
https://app.example.com/projects/acme-dashboard?view=recordingShows the exact page open when the recording-related log event is created.
Referring page
https://app.example.com/projectsShows the page that led you to the current page when the browser exposes a referrer.
Site host
app.example.comIdentifies the website where the recording-related event happened.
Path and scheme
/projects/acme-dashboard and httpsAdds the page path and whether the page used HTTP or HTTPS.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://browser-http-intake.logs.datadoghq.com/v1/input/pub829657516887adb6c583bd9646064850
Datadog browser log intake endpoint configured by the extension client token.
04EvidenceCODE COMPARE
The code that does this

The content script adds page context before sending a logger message

What it actually does
Expanded logger helperdeobfuscated/service_worker.js
const shareAnonymousDataState$1 = createPersistedState("shareAnonymousData", true);
const Log = (path) => {
  if (getEnv$1() === "content") {
    return async (type, description, context) => {
      if (path !== "recorder/src/helpers/callRemoteFunction.ts" && !inIframe()) {
        const modifiedContext = {
          ...context,
          view: {
            referrer: document.referrer,
            url: document.URL,
            url_details: {
              host: document.location?.host,
              path: document.location?.pathname,
              scheme: (document.location?.protocol || "").replace(/:$/, "")
            }
          }
        };
        return callRemoteFunction({
          to: "background",
          name: "logger",
          params: [
            {
              type,
              description,
              context: modifiedContext,
              path,
              subPackageName: "content"
            }
          ]
        });
      } else {
        try {
          if (localStorage.getItem("DEBUG_LEVEL") === "log") {
            console[type](path, description, context);
          }
        } catch {
        }
        return false;
      }
    };
  } else {
    return async (type, description, context) => {
      const shareAnonymousData = await shareAnonymousDataState$1.get();
      return Log$1(path, { uploadToDatadog: shareAnonymousData })(
        type,
        description,
        context
      );
    };
  }
};
const logger = ({
  type,
  description,
  context,
  path,
  subPackageName
}) => {
  const log = Log$1(path, { subPackageNameOverwrite: subPackageName });
  return log(type, description, context);
};
05EvidenceCODE COMPARE
The code that does this

The background logger initializes and forwards to Datadog

What it actually does
Datadog client token and initializationdeobfuscated/service_worker.js
var define_process_env_default$5 = { CHROME_EXTENSION_ENV: "background", REACT_APP_MOCK_ACTIVATION: "false", REACT_APP_API_URL: "https://internal-api.browse.ai", REACT_APP_EXTENSION_LOGIN_URL: "https://dashboard.browse.ai/extension-activate", REACT_APP_MARKETING_SITE_URL: "https://browse.ai/", REACT_APP_DASHBOARD_URL: "https://dashboard.browse.ai", REACT_APP_HELP_URL: "https://browse.ai/support", REACT_APP_DEBUG_LEVEL: "error", REACT_APP_PACKAGE_NAME: "recorder", REACT_APP_DATADOG_CLIENT_TOKEN: "pub829657516887adb6c583bd9646064850" };
const clientToken = define_process_env_default$5.DATADOG_CLIENT_TOKEN || define_process_env_default$5.REACT_APP_DATADOG_CLIENT_TOKEN;
if (clientToken && getExtensionEnv() !== "content") {
  datadogLogs.init({
    clientToken,
    datacenter: "us",
    forwardErrorsToLogs: true,
    sampleRate: 100
  });
Forwarding log context to Datadogdeobfuscated/service_worker.js
      if (!context.user && !defaultLogContext.user) {
        const user = await getUser(subPackageName);
        if (user) {
          setDefaultLogContext({
            user
          });
          context.user = user;
        }
      }
      datadogLogs.logger[type](description, {
        custom: {
          packageName,
          subPackageName,
          path,
          humanReadableDate: date.format(dateHumanReadableFormat),
          timestamp: date.valueOf(),
          context: {
            ...defaultLogContext,
            ...context
          }
        },
        stage: define_process_env_default$5.STAGE || define_process_env_default$5.REACT_APP_STAGE,
        source: "browser",
        service: `${packageName}${subPackageName !== packageName ? `/${subPackageName}` : ""}`
      });
    }
    return true;
06EvidenceTHIRD PARTY LIST
External service receiving the logging event
  • browser-http-intake.logs.datadoghq.com

    Datadog browser log intake receives extension log events that include the page context added by the content script.

Data recipients

Google SheetsZapierBrowse AI
Updated 20 September 2026obpcenkclppghkfpielmefegceegofeh