Is AI Grok Exporter - Grok to PDF, MD, and more safe?

High risk

AI Grok Exporter sends selected Grok conversation export payloads to its PDF service and OpenPanel analytics.

When you export a Grok chat to PDF, the extension sends the selected conversation content to apir.grokexporter.com for document generation and also includes the request payload in an OpenPanel analytics event. If you submit feedback, the request to the feedback API attaches the extension's full local storage, which can include account, session, and settings data beyond the visible feedback fields.

72Risk
Who publishes it

Mark - 5 other listings from the same operator, 2 of them carrying a finding

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
Mark

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

Publishers can request a review.

Findings

PDF exports send Grok conversation content to analytics

Exporting a Grok chat to PDF packages the text, page URL, account metadata, timestamps, and export options.

The background worker also logs the full PDF request to an OpenPanel event at op.grokexporter.com/api/track before dispatching.

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

You start a PDF export from a selected Grok conversation.

The content script builds the export request from checked conversation turns, account fields, timestamps, and export settings.

The extension did this

The extension records that PDF request object in an analytics event.

The background listener calls the analytics wrapper with event name Background Runtime Message and includes req:e, where e is the full runtime message.

Fields copied into the PDF request that is also attached to the analytics event
  • Selected conversation turns
    Prompt: Summarize this launch plan; Response: Here is a phased rollout plan... (illustrative)

    The export can include the text you selected for the PDF, with prompts and responses represented as separate turns.

  • Grok account identity
    Jordan Lee <jordan.lee@example.com> (illustrative)

    The request can include the name and email address associated with the export settings or page account data.

  • Conversation page URL
    https://grok.com/c/0f4e1c90-2b7a-4d5a-9a72-0dcbf86c5b31 (illustrative)

    The page address links the export to a specific Grok conversation or project chat.

  • Conversation timestamps
    createdRaw: 1720534800000, exportedRaw: 1720617625123 (illustrative)

    Creation, update, and export times show when the conversation existed and when you exported it.

  • Extension account data
    cusId: 8f8d0f9a-1f3e-4ad7-89b9-63e9c3dd4d55 (illustrative)

    Stored customer and session fields can tie the export event to the extension account record.

  • Export options
    fileName: Q3-planning, userEmail: jordan.lee@example.com (illustrative)

    PDF settings describe how the conversation was exported and can add context about the file you created.

Captured request
POSThttps://op.grokexporter.com/api/track

Observed dynamic analysis confirmed a JSON OpenPanel track request named Background Runtime Message for cmd="pdf"; no full request body was recorded in the available evidence.

The code that does this

Shipped code path from PDF export to OpenPanel analytics

Readable version

Readable equivalent of the PDF export handler

content.js (readable equivalent)
async function handleExportPdf(options) {  trackContentEvent("Content Button Click", { name: "export pdf", status: "begin" });  let jobId = null;  try {    showExportState({ show: true, type: "processing", info: null });    selectTurns(selectedTurns);    if (pendingLoad.current) await pendingLoad.current;    const exportInfo = await buildExportInfo(pageState.current, selectedTurns);    const account = await accountPromise.current;    const config = extensionConfig;    const isPro = account?.isPro;    const payload = {      cusId: account?.cusId,      email: account?.email,      dayId: currentDayId(),      exportInfo: {        name: exportInfo.name,        user: {          name: options.userName || exportInfo.user.name,          email: options.userEmail || exportInfo.user.email        },        timestamps: {          created: exportInfo.createTime,          createdRaw: exportInfo.createTimeRaw,          updated: exportInfo.updateTime,          updatedRaw: exportInfo.updateTimeRaw,          exported: new Date().toLocaleString(),          exportedRaw: new Date().getTime(),          timezoneOffset: new Date().getTimezoneOffset()        },        lst: exportInfo.lst.map((turn) => ({          role: turn.role,          sender: turn.sender,          content: turn.checked ? turn.content : null,          createTime: turn.createTime        }))      },      options    };    const response = await chrome.runtime.sendMessage({      cmd: "pdf",      payload: { cmd: "create", payload }    });    jobId = response?.jobId || null;    if (!response || response.errcode !== 0) {      throw new Error(response?.errmsg || "No response from service worker. Please try again.");    }    if (!response.jobId) {      throw new Error("No PDF job id returned. Please try again.");    }    const result = await pollPdfJob(jobId);    const link = document.createElement("a");    link.href = result.downloadUrl;    link.download = (options.fileName ? `${options.fileName}.pdf` : `${sanitizeFilename(config.fileNamePrefix)}${exportInfo.name}.pdf`).normalize("NFC");    link.click();    resetExportState();    if (config.autoCloseSelector) closeSelector(false);    if (isPro && config.muteSuccessMsg) {      showExportState({ show: false, type: null, info: null });    } else {      showExportState({ show: true, type: "success", info: { exportType: "pdf" } });    }    trackContentEvent("Content Button Click", { name: "export pdf", status: "success" });  } catch (err) {    log.error("in handleExportPDF, err:", err);    showExportState({ show: false, type: null, info: null });    const errorInfo = { errcode: -1, errmsg: "Failed to export PDF: " + err.message, stack: err.stack };    showErrorDialog({ open: true, errorInfo });    trackContentEvent("Content Button Click", { name: "export pdf", status: "failed", errmsg: err.message, ...(jobId && { jobId }) });  }}

Readable equivalent of exportInfo construction

content.js (readable equivalent)
async function buildExportInfo(pageData, selectedTurns) {  const convoData = pageData.convoData || {};  log.debug("[grok exportInfo] build start", {    conversationId: convoData.conversationId,    title: convoData.title,    createTime: convoData.createTime,    modifyTime: convoData.modifyTime,    turnCount: selectedTurns.length,    checkedCount: selectedTurns.filter((turn) => turn.checked).length  });  const exportInfo = {    name: "",    lst: [],    user: { name: "", email: "" },    createTime: "",    createTimeRaw: null,    updateTime: "",    updateTimeRaw: null,    convoLink: ""  };  if (convoData.title) {    exportInfo.name = sanitizeFilename(convoData.title).normalize("NFC");  } else {    const url = new URL(document.URL);    exportInfo.name = sanitizeFilename(document.querySelector(`a[href='${url.pathname}'] div`)?.textContent || "exporter").normalize("NFC");  }  exportInfo.convoLink = document.URL;  const storedUserInfo = await readStorage(null, "userInfo");  exportInfo.user.name = storedUserInfo?.name || pageData.userData?.user?.name || "Anonymous";  exportInfo.user.email = storedUserInfo?.email || pageData.userData?.user?.email || "";  if (convoData.createTime) {    exportInfo.createTimeRaw = new Date(convoData.createTime).getTime();    exportInfo.createTime = new Date(convoData.createTime).toLocaleString();  }  if (convoData.modifyTime) {    exportInfo.updateTimeRaw = new Date(convoData.modifyTime).getTime();    exportInfo.updateTime = new Date(convoData.modifyTime).toLocaleString();  }  exportInfo.lst = selectedTurns.map((turn, index) => ({    turnId: turn.turnId,    index,    sender: turn.sender,    role: turn.sender === "human" ? "Prompt" : "Response",    createTime: turn.createTime ? new Date(turn.createTime).toLocaleString() : "",    checked: turn.checked,    content: turn.content  }));  log.debug("[grok exportInfo] summary", {    name: exportInfo.name,    createTime: exportInfo.createTime,    updateTime: exportInfo.updateTime,    convoLink: exportInfo.convoLink,    itemCount: exportInfo.lst.length,    items: exportInfo.lst.map((turn) => ({      turnId: turn.turnId,      sender: turn.sender,      role: turn.role,      checked: turn.checked,      contentKeys: Object.keys(turn.content || {})    }))  });  log.debug("[grok exportInfo] exportInfo", exportInfo);  return exportInfo;}

Readable equivalent of the background runtime listener

background.js (readable equivalent)
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {  log.debug("in handleRuntimeOnMessage, req:", request);  log.debug("in handleRuntimeOnMessage, sender:", sender);  if (request.cmd !== "account") {    trackOpenPanelEvent("Background Runtime Message", {      status: "begin",      cmd: request.cmd,      req: request    });  }  let response = null;  if (request.cmd === "open_options_page") {    chrome.runtime.openOptionsPage();    sendResponse({ errcode: 0, errmsg: "options page opened" });  } else {    if (request.cmd === "pdf") return handlePdfMessage(request, sender, sendResponse), true;    if (request.cmd === "account") return handleAccountMessage(request, sender, sendResponse), true;    if (request.cmd === "usage") return handleUsageMessage(request, sender, sendResponse), true;    if (request.cmd === "feedback") return handleFeedbackMessage(request, sender, sendResponse), true;    log.error("wrong req.msg");    sendResponse((response = { errcode: -1, errmsg: "wrong req.msg" }));  }  log.debug("rsp:", response);});

Readable equivalent of the analytics wrapper

background.js (readable equivalent)
async function trackOpenPanelEvent(name, properties) {  try {    if (!hasOpenPanelConfig()) return;    if (!openPanelReady) await initializeOpenPanel();    const pageUrl = typeof document !== "undefined" ? document.URL : undefined;    const stored = await chrome.storage.local.get(["session"]);    const profileId = stored.session?.user?.cusId;    await getOpenPanelClient().track(name, {      ...properties,      extname: config.ext.name,      ...(pageUrl && { url: pageUrl }),      ...(profileId && { profileId })    });    log.debug("[op] Event tracked:", name, properties);  } catch (err) {    log.error("[op] catch err:", err.message);  }}function getOpenPanelClient() {  if (!hasOpenPanelConfig()) return null;  if (!client) {    client = new OpenPanel({      clientId: config.op.clientId,      clientSecret: config.op.clientSecret,      apiUrl: config.op.apiUrl,      disabled: true    });  }  return client;}class OpenPanelClient {  async send(event) {    if (this.options.filter && !this.options.filter(event)) return Promise.resolve();    if (this.shouldQueue(event)) {      this.addQueue(event);      return Promise.resolve();    }    const response = await this.api.fetch("/track", event, { keepalive: event.type !== "replay" });    this.deviceId = response?.deviceId;    const hadSession = !!this.sessionId;    this.sessionId = response?.sessionId;    if (!hadSession && this.sessionId) this.flush();    return response;  }  track(name, properties) {    this.log("track event", name, properties);    const { groups, profileId, ...rest } = properties ?? {};    const groupIds = [...new Set([...this.groups, ...(groups ?? [])])];    return this.send({      type: "track",      payload: {        name,        profileId: profileId ?? this.profileId,        groups: groupIds.length > 0 ? groupIds : undefined,        properties: { ...(this.global ?? {}), ...rest }      }    });  }}
External hosts reached by the PDF export flow
    • op.grokexporter.com

    OpenPanel analytics endpoint that receives Background Runtime Message events, including the PDF runtime message as req.

    • apir.grokexporter.com

    Grok Exporter PDF API that receives the PDF creation request for document generation.

Where it sends data

Destinations our analysis observed AI Grok Exporter - Grok to PDF, MD, and more 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.

  • apir.grokexporter.com

    AI Grok Exporter - Grok to PDF, MD, and more sends data to apir.grokexporter.com. No other extension we have analysed sends data here.

  • op.grokexporter.com

    AI Grok Exporter - Grok to PDF, MD, and more sends data to op.grokexporter.com. No other extension we have analysed sends data here.

Updated 30 September 2026gjoigaflapfgapfebiajiolclnpfneao