Is Chat with all AI models (Gemini, Claude, DeepSeek…) & AI Agents | AITOPIA safe?
AITOPIA is medium risk. When you ask the AI chat to use the page as context, the extension reads it, stores title, URL, favicon, and text, and includes that in a POST to Aitopia. Dynamic analysis didn't complete this flow, so the body is source evidence.…
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Page-context chat sends page contents to AITOPIA
When you ask the AI chat to use the page as context, the extension reads it, stores title, URL, favicon, and text, and includes that in a POST to Aitopia.
Dynamic analysis didn't complete this flow, so the body is source evidence.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You ask the AI chat to use the current page as context.
The extension reads the page and sends the page URL, title, favicon, and extracted text with the chat request.
- Current page URLhttps://example.com/account/settings?tab=security (illustrative)
Shows which page you were viewing when you asked the AI to use page context.
- Page titleAccount security settings (illustrative)
Adds the page or document title to the chat context.
- Favicon URLhttps://example.com/favicon.ico (illustrative)
Identifies the site icon associated with the page being summarized.
- Extracted page textRecent sign-in activity and recovery options are listed below (illustrative)
Includes readable text from the page so the AI can answer using that page.
Source-code confirmed POST endpoint; no ai/send body was captured during dynamic analysis because the page-context chat flow was not completed.
- Accept
- text/plain
The page-context flow reads the DOM and sends context through aiChat
Readable page-context read and send methods
assets/382f08fab624e642e26a9859ec0d1773.jsasync onReadPage() { var t; try { if (this.constants.is_web_app) return this.notify.error(this.__("You can't use this feature on this page.")); const e = f.parseMeaningfulContentFromHTML(document.documentElement.innerHTML), a = f.prepareContentForSearch(e); if (!e) { console.error("Unable to read page content."); return } await this.changeStorage("page_context", { favicon: ((t = document.querySelector('link[rel="icon"]')) == null ? void 0 : t.href) || "", title: document.title, url: window.location.href, content: a }) } catch (e) { console.error("Error reading page:", e), console.log("An error occurred while reading the page.") }},async onSelectPageOption(t, e = null) { var d, p, m, h, v, g, y, C; if (!((p = (d = this.storage.Local) == null ? void 0 : d.page_context) != null && p.title)) return; const a = t && t.requireNavigateTo == "web_search"; a ? this.onWebAccessChange(!0) : this.onWebAccessChange(!1); const n = { context: this.storage.Local.page_context.content, prompt: e ?? t.prompt }; let o = this.storage.Local.page_context.content; if (a) { const S = t.search_prefix.length; o = this.storage.Local.page_context.content.substring(0, 2040 - S), o = `${o} ${t.search_prefix??e}` } let s = { contextData: { mode: "pagecontext", favicon: (h = (m = this.storage.Local) == null ? void 0 : m.page_context) == null ? void 0 : h.favicon, title: (g = (v = this.storage.Local) == null ? void 0 : v.page_context) == null ? void 0 : g.title, url: (C = (y = this.storage.Local) == null ? void 0 : y.page_context) == null ? void 0 : C.url, actionType: t ? { id: t.id, label: t.label, description: t.description } : { id: "custom_prompt", label: "Custom Prompt", description: e.substring(0, 100) } } }; const u = a ? o : JSON.stringify(n); this.aiChat(u, this.settings.ai_model, "ai_chat", "__all", !0, !1, null, !1, s), this.changeStorage("page_context", null)}Readable content extraction helper
assets/f25cd05595e3875a792027c4772abf53.jsparseMeaningfulContentFromHTML(e) { var t, a, n, s, i, o, l; const r = (new DOMParser).parseFromString(e, "text/html"); ["script", "style", "iframe", "nav", "header", "footer", "aside", "noscript", "link", "body .ad", "body .advertisement", "body #ads", "body #ad-container", "body .social-share", "body .comments", "body .related-articles", 'body [class*="ads"]', 'body [id*="ads"]', 'body [class*="banner"]'].forEach((e => { r.querySelectorAll(e).forEach((e => e.remove())) })); const c = { importantTags: {}, meta: { title: "", description: "" }, mainContent: null }; c.meta.title = (null == (a = null == (t = r.querySelector("title")) ? void 0 : t.textContent) ? void 0 : a.trim()) || "", c.meta.description = (null == (s = null == (n = r.querySelector('meta[name="description"]')) ? void 0 : n.getAttribute("content")) ? void 0 : s.trim()) || "", ["h1", "h2", "h3", "h4", "h5", "h6"].forEach((e => { const t = r.querySelectorAll(e); t.length > 0 && (c.importantTags[e] = Array.from(t).map((e => { var t; return null == (t = null == e ? void 0 : e.textContent) ? void 0 : t.trim() })).filter((e => e)).join(" OR ")) })); const u = ["article", "main", ".content", ".post-content", ".article-content", "#main-content", '[role="main"]']; for (const d of u) { const e = r.querySelector(d); if (e) { c.mainContent = (null == (i = null == e ? void 0 : e.textContent) ? void 0 : i.trim().replace(/\s+/g, " ").replace(/\n\s*/g, "\n").replace(/\s*\n\s*\n\s*/g, "\n\n")) || null; break } } return c.mainContent || (c.mainContent = (null == (l = null == (o = null == r ? void 0 : r.body) ? void 0 : o.textContent) ? void 0 : l.trim().replace(/\s+/g, " ").replace(/\n\s*/g, "\n").replace(/\s*\n\s*\n\s*/g, "\n\n")) || null), c}Readable AI send endpoint construction
assets/f25cd05595e3875a792027c4772abf53.jslet x = `${h.api_url}/ai/send`, v = null != s.notUndefined(c, "compare_turn") || null != s.notUndefined(c, "variant_group");if (null != this.credit_detail && !v) { let e = t; if (this.settings.web_access && (e = "GPT-4o"), typeof this.credit_detail[e] < "u" && this.credit_detail[e].balance < this.credit_detail[e].cost && -999 != this.credit_detail[e].credit && "api" !== this.settings.ai_mode) return this.ai_all_response[n][a].forEach(((e, t) => { "limit" == e.role && delete this.ai_all_response[n][a][t] })), this.ai_all_response[n][a] = Object.values(this.ai_all_response[n][a]), this.ai_all_response[n][a].push({ item: "Usage Limit please upgrade plan.", role: "limit", model: t }), await this.setDataStorage(a, n), void(await this.waiting(!1))}- extensions.aitopia.ai
Receives AI chat requests that include the selected page context.
Undisclosed Amazon Affiliate Registration with captaina.co at Startup
On load, AITOPIA contacts captaina.co, absent from its listing, manifest, and allowlist, getting an Amazon affiliate ID for each of 21 stores, stored under key merchants.
DA confirmed the POST fires at startup, empty body, exposing your IP.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You install this extension or restart your browser.
The service worker contacts captaina.co and registers for an Amazon affiliate key covering 21 regional storefronts, without any notice in the extension listing or manifest.
The POST fires with an empty body, disclosing your IP address. The returned merchant IDs for all 21 Amazon regions are stored in chrome.storage.local.
200 OK, returns JSON with data.key (hopekey API key) and data.licences.markets (object mapping 21 Amazon domain strings to associate IDs); both are written to chrome.storage.local
- Content-Type
- application/json
- Your IP address (sent)203.0.113.42 (illustrative)
Disclosed via the TCP connection. The request body is empty, but captaina.co's servers still receive your IP address.
- Affiliate API key received (hopekey)f2a1b3c4d5e6f7a8b9c0d1e2f3a4b5c6 (illustrative)
A credential issued by captaina.co, stored under key hopekey, linking this browser's extension sessions to a captaina.co affiliate account.
- Amazon Marketplace IDs (21 regions){"amazon.com":"ATVPDKIKX0DER","amazon.co.uk":"A1F83G8C2ARO7P","amazon.de":"A1PA6795UKMFR9","amazon.fr":"A13V1IB3VIYZZH","amazon.co.jp":"A1VC38T7YXB528","amazon.ca":"A2EUQ1WTGCTBG2"}
Amazon associate tracking IDs per storefront, stored under merchants; insertable into Amazon URLs to attribute purchases to the account.
Automatic captaina.co registration in class E's constructor/setKey() (494d3f253ab4.js)
// Module: assets/494d3f253ab426237c2332badda6774b.js (service worker)// Instantiated unconditionally at module load — no user interaction requiredclass CaptainaClient { captaina_version = '1.1.0'; site_url = 'https://captaina.co'; api_url = 'https://_crawl.captaina.co'; // auth/logout endpoint hopekey = null; merchants = null; // Hardcoded Amazon associate IDs for 21 regional storefronts market_urls = { 'amazon.fr': 'A13V1IB3VIYZZH', 'amazon.sa': 'A17E79C6D8DWNP', 'amazon.nl': 'A1805IZSGTT6HS', 'amazon.sg': 'A19VAU5U5O7RUS', 'amazon.com.mx': 'A1AM78C64UM0Y8', 'amazon.pl': 'A1C3SOZRARQ6R3', 'amazon.co.uk': 'A1F83G8C2ARO7P', 'amazon.de': 'A1PA6795UKMFR9', 'amazon.es': 'A1RKKUPIHCS9HS', 'amazon.co.jp': 'A1VC38T7YXB528', 'amazon.in': 'A21TJRUUN4KGV', 'amazon.ca': 'A2EUQ1WTGCTBG2', 'amazon.se': 'A2NODRKZP88ZB9', 'amazon.com.br': 'A2Q3Y263D00KWC', 'amazon.ae': 'A2VIGQ35RCS4UG', 'amazon.com.tr': 'A33AVAJ2PDY3EV', 'amazon.com.au': 'A39IBJ37TRP1C6', 'amazon.com.be': 'AMEN7PMS3EDWL', 'amazon.it': 'APJ6JRA9NG5V4', 'amazon.eg': 'ARBP9OOSHTCHU', 'amazon.com': 'ATVPDKIKX0DER' }; constructor() { this.setKey(); // <-- runs immediately; contacts captaina.co on first install // Build pattern_urls list for use in content scripts on Amazon domains Object.keys(this.market_urls).forEach(domain => { this.pattern_urls.push(`https://*.${domain}/*`); this.marketplace_idBydomain[this.market_urls[domain]] = `https://www.${domain}`; }); } setKey() { // Throttle: skip if called within 1 second of last call if (this.start_key === 1 || (Date.now() - this.check_key_time) / 1000 <= 1) { return chrome.storage.local.get('hopekey'); } this.check_key_time = Date.now(); return chrome.storage.local.get('hopekey').then(stored => { if (stored.hopekey !== undefined) { // Key already exists — load it this.hopekey = stored.hopekey ?? null; if (this.hopekey == null || this.hopekey === 'false') { // Invalid key: call auth/logout on _crawl.captaina.co, clear storage fetch(this.api_url + 'auth/logout', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); chrome.storage.local.remove(['hopekey', 'merchants', 'credits']); } else { // Valid key: load previously stored merchants chrome.storage.local.get('merchants').then(m => { if (m.merchants !== undefined) this.merchants = m.merchants; }); } } else { // No key yet — register with captaina.co // Empty POST body; IP address is disclosed through the TCP connection this.start_key = 1; fetch(this.site_url + '/extensions/app/get_key', { method: 'POST', headers: { 'Content-Type': 'application/json' } }) .then(r => r.text()) .then(text => { const response = JSON.parse(text); if (response.data?.key) { // Store affiliate API key chrome.storage.local.set({ hopekey: response.data.key }); if (response.data.licences?.markets) { // Store Amazon merchant IDs for 21 regions chrome.storage.local.set({ merchants: response.data.licences.markets }); this.merchants = response.data.licences.markets; } this.hopekey = response.data.key; } this.start_key = 0; }) .catch(() => { this.start_key = 0; }); } }); }}new CaptainaClient(); // Instantiated at module top-level- captaina.co
Issues an affiliate key and 21 Amazon marketplace IDs at startup. Absent from host_permissions, externally_connectable, and the listing. Vendor relationship undocumented.
- _crawl.captaina.co
Authentication endpoint referenced in class E. Called for auth/logout when the stored hopekey is null or 'false'.
User Preferences Sent to aitopia.ai on Every Page Load
Each navigation, this extension POSTs your preferences (AI model, language, sidebar position, YouTube state), two session tokens, and the runtime ID to extensions.aitopia.ai, firing within seconds of load.
The server returns an API key.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You navigate to any webpage with this extension installed.
The extension POSTs your full settings object, including AI model, language, and UI preferences, along with two persistent session tokens to extensions.aitopia.ai.
This fires within seconds of page load, before you interact with any AI feature. The extension runtime identifier is also included in the request body.
- Session token (uuid_hopekey)4df6ae12a38d3c36e60dfd10516763ff
A persistent token that lets the server associate all your past and future requests with the same browser profile.
- Session search token (uuid_search)c7e3a1d942f8b056e2197f43a8d6c0b1e5f92847 (illustrative)
A second persistent token included in every request to the aitopia.ai backend to track your session across navigations.
- Extension runtime IDbecfinhbfclcgokjlobojlnldbfillpf
The Chrome Web Store ID of this extension, sent with every request. This is the same value for all users of the extension.
- Full settings object{"ai_model":"AITOPIA","color_mode":"dark","language":"en","sidebar_position":"right","youtube":false,"web_access":false,"context_reading":true,"context_writing":true}
Your full extension preferences: AI model, UI language, sidebar position, YouTube state, web access toggle, and reading/writing modes.
- Extension version and build typev=6.8.0, bt=self
Identifies the exact version and distribution channel of the extension you have installed.
200 OK, returns uuid_hopekey (API key), uuid_search, credit_usage, credit_detail, licences, plan, and google_auth_uri; written to chrome.storage.local
- Content-Type
- application/json
{ "ai_mode": "local", "lang": "en", "model": "AITOPIA", "settings": { "ai_model": "AITOPIA", "color_mode": "dark", "language": "en", "sidebar_position": "right", "youtube": false, "web_access": false, "context_reading": true, "context_writing": true }, "uuid_hopekey": "4df6ae12a38d3c36e60dfd10516763ff", "uuid_search": "<redacted>", "v": "6.4.0", "ri": "becfinhbfclcgokjlobojlnldbfillpf", "bt": "self"}uuid() collects settings and calls get_key; apiCall() injects runtime fields before every request
uuid() — annotated readable form
// Called by the content script on every page load initializationuuid = async (force = 0, extraData = {}) => { // Read all local storage const storage = await chrome.storage.local.get(); const settings = storage.settings ?? DEFAULT_SETTINGS; // Build request payload from stored user preferences const payload = { ai_mode: settings.ai_mode, // e.g. 'local', 'api' lang: settings.language, // e.g. 'en' model: settings.ai_model, // e.g. 'AITOPIA', 'claude-3-5-sonnet' settings: settings // the full preferences object }; // apiCall() will also append before sending: // ri = chrome.runtime.getManifest().id (extension CWS ID) // bt = build_type ('self') // v = version string ('6.8.0') // uuid_hopekey = stored per-user API key // uuid_search = stored per-user session token return this.apiCall('/extensions/app/get_key', payload, 'POST');};apiCall() — runtime identifier and session token injection (annotated)
// Executed for every non-GET request before the fetch() call:if (constants.runtime_id != null) { payload.v = constants.version; // extension version payload.ri = chrome.runtime.getManifest().id; // extension CWS ID payload.bt = constants.build_type; // 'self'}// Append per-user session tokens from storage if not already present:if (typeof payload.uuid_search === 'undefined') { payload.uuid_search = storage.Local.uuid_search; // long opaque session token}if (typeof payload.uuid_hopekey === 'undefined') { payload.uuid_hopekey = storage.Local.uuid_hopekey; // short API key}- extensions.aitopia.ai
Aitopia's backend. Receives the full settings object, two persistent session tokens, and the runtime ID on every navigation; returns an API key, credit balance, and license.
+2 more findings not shown