Is DeepSeek Exporter - Extract chat convos easily safe?

High risk

DeepSeek Exporter sends selected DeepSeek chat content to analytics and its PDF rendering service during export.

When you export a DeepSeek chat to PDF, the extension collects the selected conversation text and sends it to api.dsexporter.com for server-side PDF generation. The same export request is also logged through Amplitude analytics, which includes the selected chat content in the event data.

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 export forwards selected DeepSeek chat to Amplitude

Exporting selected DeepSeek messages to PDF has the content script build a Markdown export object and send it to the background worker, logged as a Runtime Message event.

The Amplitude client posts it to api2.amplitude.com/2/httpapi.

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

You select DeepSeek chat messages and generate a PDF export.

The extension only reaches this path after the PDF export flow is used.

The extension did this

The extension records the full PDF request as an analytics event before producing the PDF.

That request includes the selected message roles and Markdown assembled by the content script.

Fields included in the PDF runtime request
  • Selected chat Markdown
    ## Prompt: Summarize this draft contract and list unusual clauses. (illustrative)

    This is the content of the DeepSeek messages you selected for export. It can include the substance of a private conversation.

  • Message role
    Prompt

    This shows whether each exported entry came from your prompt or the assistant response.

  • Conversation title
    Exported Conversation

    This labels the exported conversation and can reveal what the chat was about.

  • Extension client ID
    e7f3b9a1-4c2d-48e6-9a34-58f1d6c2b907

    This lets the analytics event be tied back to the same browser installation over time.

Captured request
POSThttps://api2.amplitude.com/2/httpapi

Observed during dynamic analysis: an Amplitude Background Runtime Message event was emitted for cmd "pdf" after the PDF export flow.

The code that does this

PDF export request is assembled, then logged as Amplitude event properties

Readable version

Readable equivalent of the PDF export sender

content.js
async function handleClickExportPDF(account, options) {  trackContentButtonClick("Content Button Click", { name: "export pdf", status: "begin" });  try {    setDialog({ show: true, type: "processing", info: null });    const exportInfo = await getExportInfo();    const response = await chrome.runtime.sendMessage({      cmd: "pdf",      payload: {        cmd: "whatever",        payload: {          exportInfo: {            name: exportInfo.name,            lst: exportInfo.lst.map((entry) => ({              role: entry.role,              md: entry.checked ? entry.md : null            }))          },          options        }      }    });    if (response.errcode !== 0) throw new Error(response.errmsg);    const download = document.createElement("a");    download.href = response.base64Data;    download.target = "_blank";    download.download = options.fileName || `DeepSeek-${exportInfo.name}.pdf`;    download.click();    chrome.runtime.sendMessage({      cmd: "usage",      payload: { cmd: "report_and_get", payload: { cusId: account?.cusId, dayId: currentDay(), value: 1 } }    });    setDialog({ show: true, type: "success", info: null });    trackContentButtonClick("Content Button Click", { name: "export pdf", status: "success" });  } catch (error) {    setDialog({ show: false, type: null, info: null });    trackContentButtonClick("Content Button Click", { name: "export pdf", status: "failed", errmsg: error.message });  }}

Readable equivalent of selected DeepSeek message collection

content.js
async function getExportInfo() {  const info = { name: "", lst: [], user: { name: "", email: "" } };  const chatSessionId = new URL(document.URL).pathname.split("/").pop();  info.name = "Exported Conversation";  const token = JSON.parse(localStorage.getItem("userToken"))?.value;  if (!token) throw new Error("token not found");  const history = await fetch(`/api/v0/chat/history_messages?chat_session_id=${chatSessionId}`, {    headers: { Authorization: `Bearer ${token}`, "x-client-platform": "web" }  }).then((response) => response.json());  if (!history.code && history.data.biz_data.chat_session.title) info.name = history.data.biz_data.chat_session.title;  const nodes = document.querySelector("div.ds-markdown").parentElement.parentElement.childNodes;  for (let index = 0; index < nodes.length; index += 1) {    const node = nodes[index];    const entry = { index, role: index % 2 ? "Response" : "Prompt", checked: node.querySelector("input.deepseek-exporter-checkbox")?.checked };    if (!entry.checked) {      info.lst.push(entry);      continue;    }    const parsed = new DOMParser().parseFromString(node.outerHTML, "text/html");    parsed.querySelectorAll("div.md-code-block-banner").forEach((element) => element.parentElement.remove());    parsed.querySelectorAll("div.md-code-block-footer").forEach((element) => element.remove());    parsed.querySelectorAll("div.ds-button").forEach((element) => element.parentElement.remove());    parsed.querySelectorAll("div.ds-icon-button").forEach((element) => element.parentElement.remove());    parsed.querySelectorAll("pre").forEach((element) => {      const html = element.innerHTML;      while (element.firstChild) element.removeChild(element.firstChild);      element.innerHTML = `<code>${html}</code>`;    });    entry.text = htmlToText(parsed.body);    const turndown = new TurndownService({ codeBlockStyle: "fenced" });    turndown.use(gfmPlugins);    entry.md = turndown.turndown(parsed.body.innerHTML);    info.lst.push(entry);  }  return info;}

Readable equivalent of the background analytics logging path

background.js
const amplitudeTrack = amplitude.track;async function logAmplitude(eventType, properties) {  try {    await amplitudeTrack(eventType, { ...properties, extname: config.ext.name });  } catch (error) {    logger.error("[amp] catch err:", error.message);  }}chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {  logger.debug("in handleRuntimeOnMessage, req:", request);  logger.debug("in handleRuntimeOnMessage, sender:", sender);  logAmplitude("Background Runtime Message", { status: "begin", cmd: request.cmd, req: request });  if (request.cmd === "create_exportimg_page") {    chrome.tabs.create({ url: "exportimg.html" });    sendResponse({ errcode: 0, errmsg: "create exportimg page success" });  } else {    if (request.cmd === "pdf") { handlePdfMessage(request, sender, sendResponse); return true; }    if (request.cmd === "account") { handleAccountMessage(request, sender, sendResponse); return true; }    if (request.cmd === "usage") { handleUsageMessage(request, sender, sendResponse); return true; }    sendResponse({ errcode: -1, errmsg: "wrong req.msg" });  }});

Readable equivalent of the Amplitude destination constant

background.js
const AMPLITUDE_SERVER_URL = "https://api2.amplitude.com/2/httpapi";const amplitudeDefaults = {  flushMaxRetries: 12,  flushQueueSize: 200,  flushIntervalMillis: 10000,  serverUrl: AMPLITUDE_SERVER_URL,  serverZone: "US",  useBatch: false};
External analytics destination
    • api2.amplitude.com

    Receives the Amplitude HTTP API event named Background Runtime Message, including the background request properties for the PDF export message.

What it can do

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

    https://*.dsexporter.com/

  • Store data in your browser

    storage

Where it sends data

Destinations our analysis observed DeepSeek Exporter - Extract chat convos easily 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.

  • api2.amplitude.comwidely used

    DeepSeek Exporter - Extract chat convos easily sends data to api2.amplitude.com. A widely used service: 41 other extensions we have analysed send data here.

  • api.dsexporter.com

    DeepSeek Exporter - Extract chat convos easily sends data to api.dsexporter.com. No other extension we have analysed sends data here.

Updated 30 September 2026ccidioajndoplgongndfmljpcaejlnlc