Is HARPA AI: Web Automation with ChatGPT, Claude, Gemini, Grok safe?
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.…
Who publishes itHARPA AI TECHNOLOGIES OY - 1 other listing from the same operator, none carrying a finding
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.
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
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 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.
- Approximate locationSeattle, Washington (illustrative)
Lets the prompt include a city or region inferred from the network you are using.
- Collection time2026-07-14T09:30:00Z (illustrative)
Lets the extension decide whether to reuse the last location instead of requesting it again.
- Prompt sentenceUser location is Seattle, Washington. (illustrative)
Adds the location value directly into the context used for an AI chat request.
Unauthenticated traffic testing observed zero requests to this host; the shipped code constructs this GET when the signed-in {{userinfo}} parameter path runs.
Every 1 day
After a location is collected, the extension reuses it for the next day instead of requesting it on every prompt.
The chat parameter function that collects and caches location
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 }))})()- 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
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 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.
- All visible cell valuesRevenue,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 contentInternal 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 contentSSN,Employee ID\n***-**-1234,EMP-00392
Columns collapsed from view from view are exported in full alongside visible columns.
- Spreadsheet ID1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms
The unique document identifier embedded in the export URL, which identifies the specific sheet being accessed.
200 OK, CSV response containing all rows and columns including those collapsed from view in the current Sheets view (illustrative spreadsheet ID)
- Cookie
- <google session cookies — same-origin>
Sheet export functions in nj-engine.js (shipped vs. annotated)
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
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 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/*.
Cookie harvest and header injection in bg.js (shipped vs. annotated)
_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 } });}- 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.
200 OK, streaming NDJSON Grok response read by GrokSessionApi.ask()
- 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
{ "message": "<user's HARPA chat prompt>", "temporary": false, "disableMemory": true, "modelId": "auto", "fileAttachments": [], "imageAttachments": []}+2 more findings not shown