Is Open AI Chat GPT For Email - Chatgpt Email - GMPlus safe?

Critical risk

GMPlus reads users' ChatGPT session token from chat.openai.com and uses it to submit their prompts to OpenAI's backend on their behalf.

When a user asks a question through GMPlus's Gmail assistant, the extension fetches the user's own chat.openai.com session, extracts the ChatGPT access token, and uses it to submit the prompt directly to OpenAI's backend API, bypassing the extension's own usage quota. If that quota isn't exhausted, questions and email content are instead sent in cleartext to GMPlus's own server at extensiondock.com. The extension also syncs the user's Google login token to gmplus.io and relies on a bundled Kodepay SDK for subscription and payment management.

social_analyzerv2.0.21Chrome Web Store
97Risk

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

ChatGPT token used for extension requests

The service worker fetches the ChatGPT session endpoint on its fallback path, reads the access token, caches it 10s, and sends a conversation request with Bearer auth.

Dynamic analysis did not observe this path; account state was absent.

Severity
Critical unwanted
Type
Unexpected
CWE
CWE-522
Source
Dynamic sandbox
What actually happens
You did this

You start an AI email-assist flow from Gmail.

The extension did this

The extension can fetch your ChatGPT session token and use it for a ChatGPT conversation request.

Captured request
GEThttps://chat.openai.com/api/auth/session

Dynamic analysis did not capture this request; source code defines the request and token read path.

Captured request
POSThttps://chat.openai.com/backend-api/conversation

Source code sends the conversation request after the session token is read; dynamic analysis did not capture this request.

Headers
Content-Type
application/json
Authorization
Bearer access token returned by /api/auth/session
Credential and request fields used by the ChatGPT path
  • ChatGPT access token
    Illustrative JWT-shaped access token returned by the session API

    This can authorize ChatGPT API requests in the context of your signed-in OpenAI account.

  • Bearer authorization
    Authorization: Bearer access token returned by /api/auth/session

    This lets the extension attach the session token to a ChatGPT conversation request.

  • Your AI prompt text
    complete my reply email Please use professional tone to output the content of the email.

    The question you ask the extension is placed into the ChatGPT conversation request.

When this fires

When a batch threshold is hit

The service worker reuses a ChatGPT access token for a short window instead of fetching it for every request.

The code that does this

Token fetch and ChatGPT conversation request in the shipped service worker

Readable version

Service worker connection handler

deobfuscated/background.js
Se.default.runtime.onConnect.addListener(r => {    r.onMessage.addListener(async e => {      if (e.hasOwnProperty("question")) {        Se.default.storage.local.set({          question: e.question        });        try {          await ua(r, e.question, e.quickReplayInit)        } catch (t) {          console.error(t), r.postMessage({            error: t.message          }), kr.delete(Ar)        }      }    })  });  

Session token fetch

deobfuscated/background.js
async function sa() {    if (kr.get(Ar)) return kr.get(Ar);    let r = await fetch("https://chat.openai.com/api/auth/session");    if (r.status === 403) throw new Error("CLOUDFLARE");    let e = await r.json().catch(() => ({}));    if (!e.accessToken) throw new Error("UNAUTHORIZED");    return kr.set(Ar, e.accessToken), e.accessToken  }

Quota fallback that uses the token

deobfuscated/background.js
async function ua(r, e, t) {    let n = Ji(),      i = 0,      o = await _r(n);    o.hasOwnProperty(n) && (i = o[n].user_left);    let s = await Se.default.storage.local.get("has_guide"),      u = await _r("gpt_user_age"),      c = await _r("user_login_info"),      f = await _r("verify_code_pass");    if ((Object.keys(f).length == 0 || f.hasOwnProperty("verify_code_pass") && f.verify_code_pass == !1) && u.gpt_user_age != wt && u.gpt_user_age != xr && u.gpt_user_age != br && u.gpt_user_age != ht ? u = ht : u = u.gpt_user_age, s.has_guide == null || s.has_guide != !0) {      Xe({        eventType: 1000049      });      for (let a in Dr)        if (e.indexOf(a) != -1) {          r.postMessage({            text: Dr[a]          }), r.postMessage({            event: "DONE"          });          return        }    } else if (i < u) co(r, e, t, i, n);    else if (t) co(r, e, t, i, n);    else {      Xe({        eventType: 1000048      });      let a = await sa(),        d, g = () => {          d && hn(a, d, {            is_visible: !1          })        },        l = new AbortController;      r.onDisconnect.addListener(() => {        l.abort(), g()      }), await Et("https://chat.openai.com/backend-api/conversation", {        method: "POST",        signal: l.signal,        headers: {          "Content-Type": "application/json",          Authorization: `Bearer ${a}`        },        body: JSON.stringify({          action: "next",          messages: [{            id: St(),            role: "user",            content: {              content_type: "text",              parts: [e]            }          }],          model: "text-davinci-002-render",          parent_message_id: St()        }),        onMessage(x) {          if (x === "[DONE]") {            console.log("[DONE]"), r.postMessage({              event: "DONE"            }), g();            return          }          try {            let w = JSON.parse(x),              y = w.message?.content?.parts?.[0];            d = w.conversation_id, y && r.postMessage({              text: y,              messageId: w.message.id,              conversationId: w.conversation_id            })          } catch (w) {            console.log(w)          }        }      })    }  }
Destination defined by the token path
    • chat.openai.com

    OpenAI ChatGPT session and conversation API endpoints used by the extension fallback path.

Email questions posted to ExtensionDock

The extension builds an AI question from Gmail content and forwards it to its worker, which POSTs it to extensiondock.com/chatgpt/v3/question with app identifiers and a signature; testing stopped before the POST for lack of login.

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

You click an AI email action while Gmail content is open.

The extension did this

The extension builds an AI question from the email content and can post that question to ExtensionDock.

What's stored on your device

Your email content can be collected inside the extension before any external request is made.

Location
chrome.storage.local key 'question'
Contents
{  "question": "Email question text assembled from the Gmail page was stored by the background service worker before the outbound path was gated."}
Fields prepared for the ExtensionDock question request
  • Email question text
    complete my reply email Please use professional tone to output the content of the email.

    This can include the message or draft text you asked the extension to process.

  • Application identifier
    gmplus-gpt-gmail

    This identifies the extension application sending the request.

  • Streaming flag
    streaming: true

    This tells the remote service to stream the generated response back to the extension.

  • Request signature fields
    t, nonce, sign, encrypted_key, uid

    These authenticate the request to ExtensionDock but do not remove the email text from the JSON payload.

Captured request
POSThttps://extensiondock.com/chatgpt/v3/question

Dynamic analysis did not capture this POST because the extension login record was absent; source code defines the POST path.

Headers
Content-Type
application/json
The code that does this

Gmail prompt assembly and ExtensionDock POST path

Readable version

Generate Email button handler

deobfuscated/content-script.js
function GY(e) {    let [t, r] = Z("Generate Email"), [n, o] = Z(""), [i, a] = Z(!1), [s, l] = Z([]), u = p => {      l(g => {        let w = [...g];        return w[p] = !0, w      })    }, c = p => {      l(g => {        let w = [...g];        return w[p] = !1, w      })    }, d = () => {      let p = Ro();      wL.default.storage.local.get(["gpt_user_age", p]).then(g => {        let w = 0,          y = 0;        if (g.hasOwnProperty("gpt_user_age") && (w = g.gpt_user_age, w != ql && w != sa && w != wi && w != Ci && (w = Ci)), g.hasOwnProperty(p) && (y = g[p].user_left), w > y) {          let b = document.getElementById(ms),            C = document.getElementById("prompt_tags_val");          if (b.value.length == 0 && C?.value === "") {            let E = new CustomEvent("validate_input", {              detail: {                pass: !1              }            });            document.dispatchEvent(E);            return          }          Nh(), r("Generating"), u(1), document.querySelectorAll(ps, aa).length > 0 ? o("Insert Email") : o("Copy Response & Close")        } else {          let b = new CustomEvent("out_use_age", {            detail: {              api_useage: w,              user_left: y            }          });          document.dispatchEvent(b)        }      })    }, m = () => {      if (n == "Insert Email") {        let g = document.querySelectorAll(ps, aa);        EL(g[g.length - 1])      } else {        var p = document.getElementById("answer");        f(p.innerText);        let g = {          message: p.innerText        };        Xe({          eventType: 1000046,          otherParams: g        })      }    }, f = p => {      navigator.clipboard.writeText(p).then(() => {        $l()      }).catch(g => {        console.error("copy error:", g)      })    }, v = p => {      p.detail.log === "generate_done" && (c(1), r("Generate Email"), a(!0))    }, h = p => {      p.detail.log === "generate_start" && a(!1)    };    return document.addEventListener("generate_done", v), document.addEventListener("generate_start", h), N("div", {      children: [i && N(ht, {        className: n === "Insert Email" ? "insert_email_button" : "copy_email_button",        onClick: () => m(),        children: n      }), N(ht, {        type: "primary",        className: "generate_email_button",        loading: s[1],        onClick: () => d(),        children: t      })]    })  }

Gmail prompt builder

deobfuscated/content-script.js
function Nh() {    let e = document.getElementById(ms),      t = document.getElementById(od),      r = document.getElementById("email_tone"),      n = document.getElementById("email_language"),      o = document.getElementById("email_length"),      i = document.getElementById("email_model"),      a = document.getElementById("prompt_tags_val"),      s = document.getElementsByClassName(nd);    s.length > 0 && s[0].remove();    let l = e.value,      u = t.value,      d = document.getElementsByClassName(Qx)[0],      m = document.querySelector("." + Zx);    m && m.remove();    let f = q$();    d.parentNode.insertBefore(f, d), mr(N(_L, {}), f);    let v = H$("div", nd);    d.parentNode.insertBefore(v, d);    let h = W$;    Jw && n.value && (h += U$ + " use by " + n.value);    let p = `complete my email`;    l.length == 0 && u.length > 0 && (p = `complete my reply email`), r.value && (p += "    Please use " + r.value + ` tone to output the content of the email.`), n.value && (p += "    Please use " + n.value + ` as the output language.`), o.value && (p += "    Please control the length of the output content of the email to be  " + o.value + ` .`), i.value == "gpt-4" && (p += `    Writing with greater elegance.`);    let g, w;    a?.value != "" ? (g = {      message: a?.value    }, w = a?.value + p + h + ` ` + l, Xe({      eventType: 1000095,      otherParams: g    })) : (g = {      message: l    }, w = p + h + ` ` + l, Xe({      eventType: 1000017,      otherParams: g    })), mr(N(lM, {      question: w    }), v)  }

Content script sends the question to the service worker

deobfuscated/content-script.js
function sM(e) {    let [t, r] = Z(null), [n, o] = Z("");    return fe(() => {      let i = aM.default.runtime.connect(),        a = s => {          s.event && s.event === "DONE" ? document.dispatchEvent(new CustomEvent("generate_done", {            detail: {              log: "generate_done"            }          })) : document.dispatchEvent(new CustomEvent("generate_start", {            detail: {              log: "generate_start"            }          })), s.text ? r(s) : s.error === "UNAUTHORIZED" || s.error === "CLOUDFLARE" ? (document.dispatchEvent(new CustomEvent("generate_done", {            detail: {              log: "generate_done"            }          })), o(s.error)) : (document.dispatchEvent(new CustomEvent("generate_done", {            detail: {              log: "generate_done"            }          })), o("EXCEPTION"))        };      return i.onMessage.addListener(a), i.postMessage({        question: e.question,        quickReplayInit: e?.quick_replay_init || !1      }), () => {        i.onMessage.removeListener(a), i.disconnect()      }    }, [e.question]), t ? N("div", {      id: "answer",      dir: "auto",      children: N(kf, {        rehypePlugins: [          [qf, {            detect: !0          }]        ],        children: t.text      })    }) : n === "UNAUTHORIZED" ? N(cu, {      message: "Please login to ChatGpt",      description: N("p", {        children: ["If you use the webpage verification method to use this service, you need to log in to ", N("a", {          href: "https://chat.openai.com",          target: "_blank",          rel: "noreferrer",          children: "chat.openai.com"        }), " first, thank you!!!"]      }),      type: "warning",      showIcon: !0    }) : n === "CLOUDFLARE" ? N("div", {      children: N(cu, {        message: "Usage Limit Alarm",        description: 'Hello, your daily request limit has been reached. If you wish to continue using our service, please complete the website verification for ChatGPT by clicking "Verify" and after completing the verification, click "Try Again" to initiate your request again. Thank you for your support!😁😁',        type: "warning",        showIcon: !0,        action: N($r, {          direction: "vertical",          style: {            marginTop: "32px"          },          children: [N(ht, {            size: "small",            type: "primary",            onClick: pY,            style: {              width: "75px"            },            children: "Verify"          }), N(ht, {            size: "small",            type: "primary",            onClick: hY,            style: {              width: "75px"            },            children: "Try Again"          })]        })      })    }) : n ? N(cu, {      message: "Failed",      description: "Timeout retrieving generated content from ChatGPT. Please click the 'Generate Email' button to retry!!!😁😁",      type: "warning",      showIcon: !0    }) : N("p", {      className: "gpt-loading gpt-inner",      id: "chatGPTError",      children: "Waiting for GMPlus response..."    })  }

Service worker posts to ExtensionDock

deobfuscated/background.js
async function $i(r, e, t) {    let n = "gmplus-gpt-gmail",      i = "NHGNy5YFz7HeFb",      o = Xo,      s = new Date,      u = new Date(s.getUTCFullYear(), s.getUTCMonth(), s.getUTCDate(), s.getUTCHours(), s.getUTCMinutes(), s.getUTCSeconds()),      c = Math.floor(u.getTime() / 1e3),      f = Zi(),      a = Qo(16),      d = new Gi;    d.setPublicKey(o);    let g = d.encrypt(a),      l = n + ":" + i + ":" + c + ":" + f + ":" + g,      x = ea(l, a, a),      w = {        app_id: n,        t: c,        nonce: f,        sign: x,        encrypted_key: g,        uid: Zi()      },      y = 0,      p = Math.floor(Date.now() / 1e3),      _ = 0;    Et("https://extensiondock.com/chatgpt/v3/question?" + new URLSearchParams(w), {      method: "POST",      headers: {        "Content-Type": "application/json"      },      body: JSON.stringify({        text: r,        end_flag: !0,        streaming: !0      }),      onMessage(b) {        if (e) {          let v = Math.floor(Date.now() / 1e3);          _ == 0 && (_++, Xe({            eventType: 1000042,            otherParams: {              response_time: v - p            }          })), e(b)        } else t && t("There are currently many users, please try later!")      }    })  }

Quota path calls the ExtensionDock helper

deobfuscated/background.js
function co(r, e, t, n, i) {    try {      Xe({        eventType: 1000047      });      let o = 0,        s = "",        u = !1;      $i(e, c => {        if (c) {          if (o++, o == 1 && (s = "", u = !0, !t)) {            let f = {              user_left: n + 1            };            Se.default.storage.local.set({              [i]: f            })          }          try {            if (c.indexOf("[DONE]") > -1) {              r.postMessage({                event: "DONE"              });              return            } else {              let f = JSON.parse(c);              s += f.message            }          } catch (f) {            console.log(f)          }          try {            s && r.postMessage({              text: s            })          } catch (f) {            console.log(f)          }        }      }, c => {        console.log(c)      })    } catch (o) {      console.log(o)    }  }
Destination defined by the email question path
    • extensiondock.com

    Receives the extension question request for AI email processing when the ExtensionDock proxy path is available.

Kodepay API manages payment and entitlement state

The extension inits a bundled Kodepay client with fixed IDs, then uses api.kodepay.io for API-key creation, user info, payments, login registration, and order lookup.

DA didn't reach the login-gated flow; details come from shipped code.

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

You use the extension's login, account, or payment features.

The relevant code paths are tied to account setup, user-center access, payment-page opening, login registration, and order status checks.

The extension did this

The extension contacts Kodepay to manage entitlement and payment state.

Requests are built around fixed extension identifiers and a locally stored Kodepay API key.

Identifiers and request fields used in Kodepay operations
  • Application ID
    0c8f134e-26c4-11ee-ad4a-16b87f9b9f5b

    This fixed value identifies the extension's Kodepay application for remote entitlement checks.

  • Client ID
    7c8e77ac-26c4-11ee-93fe-16b87f9b9f5b

    This fixed client value links requests to the Kodepay client configured inside the extension.

  • Extension ID
    abekedpmkgndeflcidpkkddapnjnocjp

    This tells Kodepay which browser extension is making the request.

  • Kodepay API key
    kp_2a9f4c7e1b63d8a0f5c2e91b

    This stored key is attached to later Kodepay requests after the extension creates it.

  • Price ID
    prod_52c52ac9887540aa

    Payment-page requests send the selected plan identifier to Kodepay.

  • User key
    user_9f31a8c6d4e24b7a

    Login-registration requests use the user key returned by the gmplus.io synchronization step.

Captured request
POSThttps://api.kodepay.io/api/extension/new-key
Captured request
POSThttps://api.kodepay.io/api/extension/user-info
Captured request
POSThttps://api.kodepay.io/api/extension/make-order
The code that does this

Kodepay endpoints, headers, and payment calls in the extension code

Readable version

Kodepay client defines the API host and endpoint URLs

background.js
$r.kodepay = function(r, e) {  var t, n = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : "production",    i = "https://api.kodepay.io";  if (n === "development") t = i + "/sandbox";  else {    if (n !== "production") throw new Ae(Ae.MODE_KEY_ERROR);    t = i  }  var o = function() {      try {        return navigator.language      } catch {        return ""      }    }(),    s = function() {      try {        return ze.default.runtime.id      } catch {        return ""      }    }(),    u = 1e5,    c = "".concat(t, "/api/extension/new-key"),    f = "".concat(t, "/api/extension/user-managerment"),    a = "".concat(t, "/api/extension/user-info"),    d = "".concat(t, "/api/extension/search-order"),    g = "".concat(t, "/api/extension/make-order"),    l = "".concat(t, "/api/extension/client-user-login"),    x = "".concat(t, "/api/extension/loginpage"),    w = e + "_user",    y = e + "_ext_key",    p = "".concat(t, "/api/extension/pre_make_order"),    _ = "1.0.0",    b = [],    v = [],    I = 3e5,    A = 3e3,    P = "",    O = {},    L = { last_access_time: 0, limit: 5e3 },    V = { last_access_time: 0, limit: 5e3 },    C = { last_access_time: 0, limit: 5e3 };

Request wrapper adds Kodepay headers and calls fetch

background.js
function F($) {  return ee.apply(this, arguments)}function ee() {  return ee = Oe(me().mark(function $(Z) {    var re, ie = arguments;    return me().wrap(function(se) {      for (;;) switch (se.prev = se.next) {        case 0:          return re = ie.length > 1 && ie[1] !== void 0 ? ie[1] : {}, se.next = 3, q(y);        case 3:          if ((P = se.sent) || Z === c) {            se.next = 8;            break          }          return se.next = 7, K();        case 7:          P = se.sent;        case 8:          return re.headers = Y(re), se.abrupt("return", fetch(Z, re).then(function(le) {            return le.json()          }).then(function(le) {            return le          }).catch(function(le) {            throw new Ae("Network request failed: ".concat(le.status, " " ).concat(le.message))          }));        case 10:        case "end":          return se.stop()      }    }, $)  })), ee.apply(this, arguments)}function Y($) {  var Z = new Headers($.headers || {});  return Z.append("content-type", "application/json"), Z.append("application-id", r), Z.append("language", o), Z.append("plugin-id", s), Z.append("client-id", e), Z.append("api-key", P), Z.append("app-version", _), Z}

Payment-page request posts a price ID to make-order

background.js
function j() {  return j = Oe(me().mark(function $(Z) {    var re, ie, se, le, ve = arguments;    return me().wrap(function(ae) {      for (;;) switch (ae.prev = ae.next) {        case 0:          return ie = {            price_id: Z          }, (re = ve.length > 1 && ve[1] !== void 0 ? ve[1] : null) && (W(re) ? ie.origin_data = re : console.error("extra_info is not a JSON object")), ae.next = 5, F(g, {            method: "POST",            body: JSON.stringify(ie)          });        case 5:          if (se = ae.sent, parseInt(se.code) !== u) {            ae.next = 10;            break          }          return ae.abrupt("return", se.data);        case 10:          throw le = "get payment url error: ".concat(JSON.stringify(se)), se.code && se.message && (le = "".concat(se.code, ": ".concat(se.message))), new Ae(le);        case 13:        case "end":          return ae.stop()      }    }, $)  })), j.apply(this, arguments)}

Price page click sends the selected plan to the background code

price.js
document.addEventListener("click", function(s) {  let f = s.target;  f.id != "" && (f.id == "plan-pro" ? (yt({    message: {      message: "pay",      planid: _t    },    from: "content-script"  }), Fe({    eventType: 100065  })) : f.id == "plan-basic" ? (yt({    message: {      message: "pay",      planid: gt    },    from: "content-script"  }), Fe({    eventType: 100062  })) : f.id == "plan-free" && window.open("https://mail.google.com/mail/u/0/#inbox?compose=new"))});
Remote services for entitlement and payment operations
    • api.kodepay.io

    Receives Kodepay API-key creation, user information, payment creation, login-page, login-registration, and order-status requests.

    • kodepay.io

    Sends payment and login completion messages that the bundled Kodepay content listener forwards to the extension runtime.

+1 more finding not shown

What it can do

Permissions this extension asks for, as declared in version 2.0.21. 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 openai.com

    https://*.openai.com/

  • Read and change your data on mail.google.com

    https://mail.google.com/*

  • Store data in your browser

    storage

Where it sends data

Destinations our analysis observed GMPlus contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.

  • chat.openai.comwidely used

    GMPlus sends data to chat.openai.com. A widely used service: 27 other extensions we have analysed send data here.

  • gmplus.io

    GMPlus sends data to gmplus.io. 2 other extensions we have analysed send data here.

  • api.kodepay.io

    GMPlus sends data to api.kodepay.io. One other extension we have analysed sends data here.

  • extensiondock.com

    GMPlus sends data to extensiondock.com. One other extension we have analysed sends data here.

Updated 21 September 2026abekedpmkgndeflcidpkkddapnjnocjp