Is Sidebar: ChatGPT, Bookmarks, GPT-4o | Meomni safe?

Medium risk

Meomni Sidebar sends full page text, selected text, and Gmail thread content to its backend server on demand.

When users interact with the AI assistant, the extension reads the active tab's full page text (title, URL, and extracted body content) and any selected text, then transmits this data to v2-mlqv2syt5a-uc.a.run.app as AI prompt context. The extension also scrapes Gmail inbox metadata via Atom feed and extracts full Gmail conversation threads — including message bodies, sender emails, and attachment names — which are included in chat requests to the same backend. Additionally, it strips Content-Security-Policy and X-Frame-Options headers from all sub-frame requests across every site the user visits.

Rendomv5.0.42Chrome Web Store
45Risk
Who publishes it

Rendom - 8 other listings 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.

Publisher
Rendom
Declared legal entity
Rendom
Registered address
str. 188, 3/1, Nur-sultan, Astana 010000, KZ
Registered contact
Ruslan Zhunussov

Shared hosts - 2 hostnames

Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.

api.ticktick.com
Also called by 3 other listings, including One-Click TickTick
cang.baidu.com
Also called by 5 other listings, including NIM

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

Publishers can request a review.

Findings

Active Page Text Sent as AI Context

HighunwantedCWE-359Dynamic sandbox

Code analysis shows using the extension's Web AI actions collects the active tab's URL, page title, and page text, posted to the Meomni AI backend.

What actually happens

You did this

You trigger a Web AI action in the extension.

The overlay code configures Web actions with active-page context before calling the AI workflow.

The extension did this

The extension reads the current page and prepares it for an AI chat request.

The service worker asks the content script for page HTML, extracts readable text, and carries the URL and title into the prompt context.

Fields assembled for the AI context

  • Current page URL
    https://company.example/wiki/2026-product-plan (illustrative)

    Identifies the exact page you asked the extension to summarize or process.

  • Page title
    2026 Product Plan - Internal Wiki (illustrative)

    Adds a readable label for the page you are viewing.

  • Extracted page text
    Launch goals, customer notes, and draft timeline for Q3 planning (illustrative)

    Can include the article, document, ticket, or form text visible in the active page.

Captured request

POSThttps://v2-mlqv2syt5a-uc.a.run.app/api/v2/ai/chat

Traffic testing reached a sign-in wall before the AI chat path ran; no request body was captured.

Headers
x-app-id
fnfdomooadjpfohbepiaonnbdmkdjiog
x-app-version
5.0.27

The code that does this

Shipped code path from Web AI action to page-text POST

Readable version

Web actions attach active-tab context before calling the AI workflow

assets/useOverlay-EAeRTiwI.js
h = a(() => {  if (!r.value?.startsWith("https")) return null;  let t = {    author: "system",    title: "Web",    templates: ["summarize", "tldr"],    userContext: [{      type: "active-tab-content",      value: ""    }]  };  const e = l(o.activeTab?.url, y.value);  return e && (t = e), t.templates = t.templates.map(t => ({    id: t,    ...o.ai_getTemplate(t).meta || {}  })), t});return {  isActionLoading: i,  pageAction: async function(t, e) {    try {      if (i.value = t, !o.isTruePlus) return void o.dispatch("actionOpenGetPlusModal");      o.dispatch("frontAction_navigate", {        routePreset: "gpt"      });      await o.dispatch("actionCallLlm", {        templateId: t,        ...e?.userContext && {          userContext: e.userContext        },        save: !0,        options: {          activeModel: "advanced"        }      })    } catch (a) {} finally {      i.value = null    }  },  activeHostRule: h,  overlayAiTemplates: v,  overlayApps: m,  allApps: p,  allAiTemplates: d,  updateOverlay: function(t) {    o.dispatch("commitAndSave", {      commit: "overlay$merge",      payload: t,      to: "overlay"    })  }}

The content script returns the page HTML

contentScript/contentScript.js
case "getRawHtml":  return Promise.resolve(document.documentElement.outerHTML);

The service worker parses the active tab and returns title, URL, and textContent

assets/background.js--ICHH-ml.js
actionGetActiveTabReader: async (t, e) => {  const n = await l();  let r = {    url: n?.url,    textContent: "",    title: n?.title || "",    excerpt: "",    length: 0,    lang: "",    content: ""  };  if (!n?.id || !n?.url || !n.url.startsWith("http")) return r;  const i = await u(n.id, {    to: "contentScript",    name: "getRawHtml"  });  if (!i) return r;  const a = await t.dispatch("offScreenAction", {    name: "readability-parse",    data: {      html: i,      url: n.url    }  }).catch(t => null);  if (!a) return r;  let s = a.textContent || "";  return s && (s = lt(s)), {    ...a,    url: n.url,    textContent: s,    title: a.title || n.title || "",    excerpt: a.excerpt || "",    length: a.length || 0,    lang: a.lang || "",    content: a.content || ""  }}

ACTIVE_TAB_CONTENT is copied into the prompt context fields

assets/background.js--ICHH-ml.js
getVariableResult: async (t, e) => {  const {    type: n,    value: r  } = e;  let i = {    type: n,    content: r  };  switch (n) {    case it.TemplateTypes.VariableType.SELECTED_TEXT: {      const e = await t.selectedText.value || "";      i.content = e;      break    }    case it.TemplateTypes.VariableType.ACTIVE_TAB_CONTENT: {      const e = await t.dispatch("actionGetActiveTabReader");      if (!e?.textContent) throw new Error("Web page text content is not available");      e?.title && (i.title = e.title), e?.url && (i.url = e.url), e?.textContent && (i.content = e?.textContent);      break    }    case it.TemplateTypes.VariableType.YOUTUBE_CAPTIONS: {      const e = await t.dispatch("actionYoutubeGetCaptions");      i.content = JSON.stringify(e[r || "text"]), i.url = t.activeTab.url || "", i.title = t.activeTab.title || "";      break    }    case it.TemplateTypes.VariableType.DIALOG:      i.content = r || await t.dispatch("scraperGetConversation");      break;    case it.TemplateTypes.VariableType.FILE_DOCUMENT:      i.content = r ? await t.dispatch("frontAction_urlToText", r) : "", e?.filename && (i.filename = e.filename)  }  return i}

The AI client posts the assembled data to ai/chat

assets/background.js--ICHH-ml.js
apiClient: {  call: async (e, n) => {    try {      let i;      return i = t.ai_useApiKey ? await t.dispatch("openaiApiDirect", {        ...e,        options: {          ...e.options,          activeModel: (r = e.options?.activeModel, {            standard: "gpt-4o-mini",            advanced: "gpt-4o",            pro: "gpt-5"          } [r] || "gpt-4o-mini")        }      }) : await t.dispatch("meomniApi", {        route: n ? "ai/chat/stream" : "ai/chat",        method: "POST",        data: e,        onStream: n      }), i?.choices || i?.usage ? et.extractDataFromResponse(i) : i    } catch (i) {      throw i    }    var r  }}

The vendor backend base URL used by meomniApi

assets/mainConfig-D0q9gJWx.js
import {  b as e} from "./object-T1iCcc7Q.js";const t = "https://v2-mlqv2syt5a-uc.a.run.app/api/v2",  a = {    microsoft: {      redirectUrl: `${t}/oauth/microsoft/redirect`,      clientId: "b6906661-2c9c-428e-8323-bd0e81ebf016"    },    ticktick: {      redirectUrl: "https://us-central1-taberium-backend.cloudfunctions.net/main/api/oauth-redirect-ticktick",      clientId: "FXuG8s0c7CKyMx2KCx"    },    todoist: {      redirectUrl: "https://v2-mlqv2syt5a-uc.a.run.app/api/v2/oauth/todoist/redirect",      clientId: "3149ceccf5d944379621cb77d9f7e6e1"    }  }

Destination host

    • v2-mlqv2syt5a-uc.a.run.app

    Meomni backend that receives the AI chat request assembled by the extension code.

Gmail thread contents sent to AI chat endpoint

MediumunwantedCWE-200Dynamic sandbox

When the Gmail feature runs, it reads the visible conversation, extracts sender, text, timestamps, and attachment names, then sends that context to the AI endpoint.

What actually happens

You did this

You use the extension's Gmail response feature while viewing a Gmail conversation.

The extension did this

The extension reads the visible Gmail thread and sends the parsed conversation as AI chat context.

Gmail fields parsed from the page before the AI request

  • Sender name and email
    Maya Patel <maya.patel@example.com> (illustrative)

    This can identify who is participating in the email thread you are viewing.

  • Message body text
    Hi Maya, the Q3 launch deck is attached. Can you review before 3 PM? (illustrative)

    This can include the private contents of the email messages visible in the thread.

  • Message timestamp
    Jul 12, 2026, 10:42 AM (illustrative)

    This shows when messages in the thread were sent or received.

  • Attachment names
    Q3-launch-plan.pdf (illustrative)

    File names can reveal projects, customers, or subjects discussed in the thread.

Captured request

POSThttps://v2-mlqv2syt5a-uc.a.run.app/api/v2/ai/chat

The code that does this

The shipped code gates on Gmail, scrapes the thread, and sends it to AI chat

Readable version

Gmail side-panel trigger and AI context call

assets/AiRespond-DJhiZIU6.js
const xe = a(() => ["mail.google.com"].some(e => oe.value?.includes(e)));async function fe() {  try {    ne.value = !0;    const e = await Y.dispatch("scraperGetConversation");    if (ie.value = e.conversation, ie.value) {      const e = ie.value[ie.value.length - 1];      X.value = e.content;      const a = await Y.dispatch("actionCallLlm", {          templateId: "analyzeIncomingMessage",          save: !1,          context: [{            type: "conversation",            value: JSON.stringify(ie.value)          }],          options: {            activeModel: "advanced"          }        }),        {          structured: l        } = a;      l?.key_points && (re.value = l.key_points, ue.value = l.summary, de.value = l.deep_answers, ce.value = l.general_answers), l?.incoming_language && pe("language", l.incoming_language), ne.value = !1    }  } catch (e) {} finally {    ne.value = !1  }}

Gmail DOM extraction

assets/background.js--ICHH-ml.js
scraperGetConversation: async (t, e) => {  const n = await l();  if (!n?.url) return;  let r = [];  return n?.url?.includes("mail.google.com") && (r = await t.dispatch("scraperGetGmailConversation")), {    conversation: r  }},scraperGetGmailConversation: async (t, e) => {  const n = await l();  if (!n?.url) return;  const r = await Py.querySelector({    tabId: n.id,    selector: '[role="list"]',    method: "html",    fetchMethod: "tab"  });  if (!r) return;  return function(t) {    try {      const e = [".yj6qo.ajU", ".HOEnZb.adL", ".gmail_quote", ".gmail_extra", ".adM", ".ajv", ".h5", ".zFot5d", ".yj6qo", ".adL"],        n = st(t);      e.forEach(t => {        n(t).remove()      });      const r = [],        i = n('[role="listitem"]');      return i.each((t, e) => {        const a = {};        a.isLastMessage = t === i.length - 1;        const s = n(e).find("span.gD");        s.length && (a.sender = {          name: s.attr("name") || s.text().trim() || "",          email: s.attr("email") || ""        });        const o = n(e).find(".a3s.aiL");        if (o.length) {          let t = o.text().trim();          t = t.replace(/\\n/g, "\n"), t = t.replace(/\s*\n\s*/g, "\n"), t = t.replace(/\n{3,}/g, "\n\n"), t = t.replace(/\s{2,}/g, " "), a.content = t        }        const c = n(e).find("[title]");        c.length && (a.timestamp = c.attr("title") || "");        const l = n(e).find(".aQH");        l.length && (a.hasAttachments = !0, a.attachmentNames = [], l.each((t, e) => {          const r = n(e).find(".aVK").text().trim();          r && a.attachmentNames?.push(r)        })), (a.content || a.sender && a.sender.email) && r.push(a)      }), r    } catch (e) {      return []    }  }(r)},

AI chat route and backend base URL

assets/background.js--ICHH-ml.js and assets/mainConfig-D0q9gJWx.js
apiClient: {  call: async (e, n) => {    try {      let i;      return i = t.ai_useApiKey ? await t.dispatch("openaiApiDirect", {        ...e,        options: {          ...e.options,          activeModel: (r = e.options?.activeModel, {            standard: "gpt-4o-mini",            advanced: "gpt-4o",            pro: "gpt-5"          } [r] || "gpt-4o-mini")        }      }) : await t.dispatch("meomniApi", {        route: n ? "ai/chat/stream" : "ai/chat",        method: "POST",        data: e,        onStream: n      }), i?.choices || i?.usage ? et.extractDataFromResponse(i) : i    } catch (i) {      throw i    }    var r  }}const t = "https://v2-mlqv2syt5a-uc.a.run.app/api/v2";

Remote service that receives the AI chat request

    • v2-mlqv2syt5a-uc.a.run.app

    Receives the extension's AI chat request at /api/v2/ai/chat, including the conversation context assembled from Gmail.

Where it sends data

Destinations our analysis observed Meomni Pro 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.

  • v2-mlqv2syt5a-uc.a.run.app

    Meomni Pro sends data to v2-mlqv2syt5a-uc.a.run.app. One other extension we have analyzed sends data here.

Updated 15 October 2026fnfdomooadjpfohbepiaonnbdmkdjiog