Is DeepSeek Exporter - Extract chat convos easily safe?
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.
Who publishes itMark - 5 other listings from the same operator, 2 of them carrying a finding
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.
Same store account
5 other listings published from this account, 216k+ users between them. 2 of them carry a finding.
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
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 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.
- 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 rolePrompt
This shows whether each exported entry came from your prompt or the assistant response.
- Conversation titleExported Conversation
This labels the exported conversation and can reveal what the chat was about.
- Extension client IDe7f3b9a1-4c2d-48e6-9a34-58f1d6c2b907
This lets the analytics event be tied back to the same browser installation over time.
Observed during dynamic analysis: an Amplitude Background Runtime Message event was emitted for cmd "pdf" after the PDF export flow.
PDF export request is assembled, then logged as Amplitude event properties
Readable equivalent of the PDF export sender
content.jsasync 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.jsasync 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.jsconst 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.jsconst 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};- 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.