Is HARPA AI: Web Automation with ChatGPT, Claude, Gemini, Grok safe?

Medium risk

HARPA AI is medium risk. When HARPA AI expands the {{userinfo}} chat parameter, it requests whatismyipaddress.com, parses location text, stores it a day, and adds it to prompt context. Unauthenticated testing missed this path; it needs a signed-in chat interaction.…

HARPA AI LLCv14.4.0Chrome Web Store
52Risk
Who publishes it

HARPA AI TECHNOLOGIES OY - 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
HARPA AI LLC
Declared legal entity
HARPA AI TECHNOLOGIES OY
Registered address
Vesirattaantie 4, B8, Espoo 02740, FI
Registered contact
HARPA AI

Same store account

1 other listing published from this account, 1k+ 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

HARPA AI adds IP-derived location to chat prompts

When HARPA AI expands the {{userinfo}} chat parameter, it requests whatismyipaddress.com, parses location text, stores it a day, and adds it to prompt context.

Unauthenticated testing missed this path; it needs a signed-in chat interaction.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You use a signed-in HARPA AI chat flow that expands user information.

The location lookup is tied to the {{userinfo}} chat parameter, not ordinary page browsing.

The extension did this

The extension requests an external IP-location page and adds the parsed location to prompt context.

The code stores the parsed location with a timestamp and reuses it for up to one day.

Location data created by the chat parameter
  • Approximate location
    Seattle, Washington (illustrative)

    Lets the prompt include a city or region inferred from the network you are using.

  • Collection time
    2026-07-14T09:30:00Z (illustrative)

    Lets the extension decide whether to reuse the last location instead of requesting it again.

  • Prompt sentence
    User location is Seattle, Washington. (illustrative)

    Adds the location value directly into the context used for an AI chat request.

Captured request
GEThttps://whatismyipaddress.com/

Unauthenticated traffic testing observed zero requests to this host; the shipped code constructs this GET when the signed-in {{userinfo}} parameter path runs.

When this fires

Every 1 day

After a location is collected, the extension reuses it for the next day instead of requesting it on every prompt.

The code that does this

The chat parameter function that collects and caches location

Readable version

Readable location-aware userinfo parameter

bg.js
(() => {  const {    $chat: t,    $utils: n,    $s: a  } = e;  t.paramUserinfo = async () => {    if (a.global.userinfo) return a.global.userinfo;    return (await Promise.all([o(), r()])).filter(Boolean).join(" ")  };  const o = () => {      if (!a.billing.loggedIn) return;      const e = [a.billing.account.firstName, a.billing.account.lastName].filter(Boolean).join(" ");      return e ? `User name is ${e}.` : void 0    },    r = async () => {      if (Date.now() - (a.billing.locationCollectedOn || 0) < n.time.DAY) return a.billing.location ? `Current location ${a.billing.location}.` : null;      try {        const e = await fetch("https://whatismyipaddress.com/"),          t = await e.text(),          n = /<p class="information">(.+?)<\/p>/g,          o = [];        let r;        for (; null !== (r = n.exec(t));) o.push(r[1]);        const i = o.at(-1).split("<span>").at(-1).split("</span>")[0];        return a.billing.location = i, a.billing.locationCollectedOn = Date.now(), `User location is ${a.billing.location}.`      } catch (e) {        return t.warn("failed fetching location", e), a.billing.location = null, a.billing.locationCollectedOn = Date.now(), null      }    }})()

Readable registration of the userinfo chat parameter

bg.js
(() => {  const {    $chat: t,    $browser: n,    $utils: a  } = e;  t.params = [{    key: "proPlan",    value: t.paramProPlan  }, {    key: "connection",    value: t.paramConnection,    isObject: !0  }, {    key: "userinfo",    value: t.paramUserinfo  }, {    key: "date",    value: t.paramDate  }, {    key: "time",    value: t.paramTime  }, {    key: "language",    value: t.paramLanguage  }, {    key: "title",    value: t.paramPageTitle  }, {    key: "url",    value: t.paramPageUrl  }, {    key: "domain",    value: t.paramPageDomain  }, {    key: "desc",    value: t.paramPageDesc  }, {    key: "input",    value: t.paramPageInput  }, {    key: "selection",    value: t.paramPageSelection  }, {    key: "email",    value: t.paramPageEmail,    isObject: !0  }, {    key: "thread",    value: t.paramPageThread,    isObject: !0  }, {    key: "transcript",    value: t.paramYouTubeTranscript,    plain: !1  }, {    key: "transcriptPlain",    value: t.paramYouTubeTranscript,    plain: !0  }, {    key: "serp",    value: t.paramSerp  }, {    key: "message",    value: t.paramMessage  }, {    key: "cost",    value: t.paramCost  }, {    key: "gpt",    value: t.paramGpt  }, {    key: "grab",    value: t.paramGrab  }, {    key: "page",    value: t.paramPage,    preprocess: t.paramPagePreprocess  }, {    key: "view",    value: t.paramView  }, {    key: "assert",    value: t.paramAssert  }];  t.params.forEach(((e, t) => {    let n = `{{(?<key>${a.string.escapeRegExp(e.key)})`;    e.isObject && (n += "(?<field>\\.[^{} ]+)?"), n += "( (?<args>[^{}]+))?}}", e.regex = new RegExp(n, "g"), e.order = t  }))})()
External service contacted by the location parameter
    • whatismyipaddress.com

    Receives the extension's GET request and returns an HTML page whose location paragraph is parsed into chat prompt context.

Full Google Sheet Content Forwarded to AI Backend on Each Chat Message

Opening HARPA's AI chat on a Google Sheets tab and sending a message fetches the full spreadsheet via Google's CSV export API using your session cookies, including rows off screen.

The CSV is forwarded to the AI model as chat context.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You send any AI chat message via HARPA while viewing a Google Sheets document.

This includes the HARPA sidebar, slash commands, and automated agent steps that reference the current page.

The extension did this

The extension fetches the full spreadsheet as CSV using your session, including rows/columns off screen, and includes it in the message sent to the AI model.

The export uses the Google Sheets /export?format=csv endpoint with credentials:'same-origin', which returns all cell data regardless of visibility or scroll position.

Data from your Google Sheet that HARPA includes in the AI chat context
  • All visible cell values
    Revenue,Q1,Q2,Q3,Q4\nProduct A,142000,198000,231000,287000

    Every cell in the current sheet, regardless of whether it fits on your screen.

  • Collapsed from view row content
    Internal margin: 43%, Cost basis: $83,000

    Rows you have collapsed from view in the sheet view are still included in the export; they are not filtered out.

  • Collapsed from view column content
    SSN,Employee ID\n***-**-1234,EMP-00392

    Columns collapsed from view from view are exported in full alongside visible columns.

  • Spreadsheet ID
    1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms

    The unique document identifier embedded in the export URL, which identifies the specific sheet being accessed.

Captured request
GEThttps://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms/export?format=csv&gid=0

200 OK, CSV response containing all rows and columns including those collapsed from view in the current Sheets view (illustrative spreadsheet ID)

Headers
Cookie
<google session cookies — same-origin>
The code that does this

Sheet export functions in nj-engine.js (shipped vs. annotated)

Readable version

fetchExportCsv — main export path

// nj-engine.js: fetches the full Google Sheet as CSV using the authenticated sessionasync fetchExportCsv(spreadsheetId, gid) {  const url = `https://docs.google.com/spreadsheets/d/${spreadsheetId}/export?format=csv&gid=${gid || this.getActiveGid()}`;  const response = await fetch(url, { credentials: 'same-origin' }); // uses Google session cookies  if (!response.ok) throw new Error(`GSheets export failed: ${response.status} ${response.statusText}`);  return response.text(); // returns full CSV including collapsed from view rows and columns}

fetchCsv — alternative path using gviz endpoint

// Alternative CSV export using the Google Visualization API endpointasync fetchCsv(spreadsheetId, options = {}) {  let url = `https://docs.google.com/spreadsheets/d/${spreadsheetId}/gviz/tq?tqx=out:csv`;  if (options.sheet) {    url += `&sheet=${encodeURIComponent(options.sheet)}`;  } else {    url += `&gid=${options.gid || this.getActiveGid()}`; // defaults to active tab  }  if (options.range) url += `&range=${encodeURIComponent(options.range)}`;  const response = await fetch(url, { credentials: 'include' }); // includes all cookies  // ... parses and returns CSV}

Grok Session Cookies Harvested and Used to Spoof Request Origin Headers

HARPA reads all grok.com cookies (session token, cf_clearance) when Grok is selected, then rewrites Origin/Referer on grok.com/rest/app-chat/* via declarativeNetRequest, spoofing same-origin to send prompts on the user's session (bg.js).

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You select Grok as your AI backend in HARPA's settings and send a chat message.

The Grok integration is user-configured; it does not activate unless the user explicitly selects it.

The extension did this

The extension reads all of your grok.com session cookies and registers a request-header rewriting rule that makes extension-originated requests appear to come from grok.com itself.

Cookie collection includes the Cloudflare bot-clearance token (cf_clearance) and cookies stored in partitioned storage. The DNR rule overwrites Origin, Referer, and Sec-Fetch-Site headers on all xmlhttprequest traffic matching grok.com/rest/app-chat/*.

The code that does this

Cookie harvest and header injection in bg.js (shipped vs. annotated)

Readable version

_getAllCookies — collects all grok.com cookies including partitioned and cf_clearance

// bg.js GrokSessionApi: reads every grok.com cookie the browser holdsasync _getAllCookies() {  const seen = new Map;  const merge = cookies => cookies.forEach(c => { if (!seen.has(c.name)) seen.set(c.name, c); });  // Standard domain cookies  merge(await chrome.cookies.getAll({ domain: '.grok.com' }));  merge(await chrome.cookies.getAll({ url: 'https://grok.com' }));  // Partitioned (CHIPS) cookies  try {    merge(await chrome.cookies.getAll({      domain: '.grok.com',      partitionKey: { topLevelSite: 'https://grok.com' }    }));  } catch {}  // Cloudflare bot-clearance token specifically (also partitioned)  try {    const cfCookie = await chrome.cookies.get({      url: 'https://grok.com',      name: 'cf_clearance',      partitionKey: { topLevelSite: 'https://grok.com' }    });    if (cfCookie) seen.set(cfCookie.name, cfCookie);  } catch {}  return [...seen.values()];}

_injectHeaders — builds cookie string and registers DNR header-rewrite rule

// Registers a declarativeNetRequest session rule that rewrites headers on grok.com API trafficasync _injectHeaders() {  const headers = [];  // Attach all harvested grok.com cookies to every matching request  const cookies = await this._getAllCookies();  if (cookies.length) {    headers.push({      header: 'cookie',      operation: 'set',      value: cookies.map(c => `${c.name}=${c.value}`).join('; ')    });  }  // Spoof browser security headers to make requests appear same-origin  headers.push(    { header: 'origin',         operation: 'set', value: 'https://grok.com' },    { header: 'referer',        operation: 'set', value: 'https://grok.com/' },    { header: 'sec-fetch-dest', operation: 'set', value: 'empty' },    { header: 'sec-fetch-mode', operation: 'set', value: 'cors' },    { header: 'sec-fetch-site', operation: 'set', value: 'same-origin' }  );  // Apply rule to all XHR traffic matching grok.com/rest/app-chat/*  await declarativeNetRequest.register({    key: this._ruleKey, // 'grok-headers'    condition: {      urlFilter: 'https://grok.com/rest/app-chat/*',      resourceTypes: ['xmlhttprequest']    },    action: {      type: 'modifyHeaders',      requestHeaders: headers    }  });}
Cookies the extension collects from grok.com
  • Grok session cookies (sso, etc.)
    sso=eyJhbGci... (JWT-format session token)

    Authentication tokens that identify you as a logged-in Grok user and authorize API access.

  • Cloudflare clearance (cf_clearance)
    cf_clearance=Xbf7kP2..._1751059200.0.0.0

    Cloudflare clearance token issued after a browser challenge. Bypasses rate-limiting and anti-bot checks when attached to requests.

  • Partitioned storage cookies
    __Secure-grok-session-partitioned=...

    Cookies stored under the Storage Access API partition for grok.com, which may include additional session or preference state.

Captured request
POSThttps://grok.com/rest/app-chat/conversations/new

200 OK, streaming NDJSON Grok response read by GrokSessionApi.ask()

Headers
Cookie
<all grok.com cookies including sso and cf_clearance — set by DNR rule>
Origin
https://grok.com
Referer
https://grok.com/
Sec-Fetch-Site
same-origin
Body
{  "message": "<user's HARPA chat prompt>",  "temporary": false,  "disableMemory": true,  "modelId": "auto",  "fileAttachments": [],  "imageAttachments": []}

+2 more findings not shown

Updated 30 September 2026eanggfilgoajaocelnaflolkadkeghjp