Is ExportGPT: Export ChatGPT Conversation safe?
ExportGPT is medium risk. With ChatGPT message blocks present, ExportGPT builds an HTML document from the conversation and posts it as JSON to snipcap.pro/api/screenshot. Testing opened the dialog but did not trigger this since no message blocks were present.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Screenshot export sends ChatGPT HTML to snipcap.pro
With ChatGPT message blocks present, ExportGPT builds an HTML document from the conversation and posts it as JSON to snipcap.pro/api/screenshot.
Testing opened the dialog but did not trigger this since no message blocks were present
You choose the screenshot export action while viewing a ChatGPT conversation.
The code requires message blocks to be present before the screenshot request is built.
The extension packages selected rendered conversation HTML for a remote screenshot service.
The prepared JSON request targets https://snipcap.pro/api/screenshot.
| Field | Value | Why it matters | |
|---|---|---|---|
Rendered conversation HTML | <!DOCTYPE html><html lang="en-US"><body><div data-message-author-role="user">Summarize the Q2 renewal terms for Acme.</div></body></html> (illustrative) | Includes the selected ChatGPT messages as page markup, so conversation text can be sent as part of the screenshot job. | |
Screenshot width | 800 | Tells the service how wide to render the screenshot. | |
Screenshot height | 600 | Tells the service how tall to render the screenshot. | |
Return type | url | Asks the service to return a URL for the generated screenshot. | |
ChatGPT asset references | https://chatgpt.com/backend-api/files/file-service-123/download (illustrative) | Stylesheet and image references can add page context to the HTML sent for rendering. |
| Content-Type | application/json |
The content script builds conversation HTML and posts it to snipcap.pro
function Ct(e, t, A) {
if (!Array.isArray(t) || 0 === t.length || !A) return "";
const n = new Array(A).fill(!1);
return t.forEach(e => {
e >= 0 && e < A && (n[e] = !0)
}), Nt(bt(e, n))
}
function Ht() {
return function() {
const e = document.documentElement.outerHTML,
t = /<link\s+[^>]*rel=["']stylesheet["'][^>]*href=["']([^"']+)["'][^>]*>/g,
A = [...e.matchAll(t)];
return A.map(e => {
const t = e[1];
return t.startsWith("/") ? window.location.origin + t : t.startsWith("http://") || t.startsWith("https://") ? t : window.location.origin + "/" + t
})
}().map(e => `<link rel="stylesheet" href="${e}">`).join("\n")
}
function Nt(e) {
const t = Ht(),
A = function(e) {
let t = e.attributes,
A = [];
for (let e = 0; e < t.length; e++) {
let n = t[e];
A.push(`${n.name}="${n.value}"`)
}
return A.join(" ")
}(document.documentElement);
return `<!DOCTYPE html>\n <html ${A}>\n \n <head>\n <meta charset="UTF-8">\n <meta http-equiv="X-UA-Compatible" content="IE=edge">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <title>${document.title}</title>\n ${t}\n <style>\n .whitespace-pre-wrap {\n white-space: unset;\n }\n </style>\n </head>\n \n <body>\n ${e}\n </body>\n \n </html>`
}
const tA = "https://snipcap.pro/api";
async function AA(e, t = 0, A = 0, n) {
const r = {
html: e,
width: t,
height: A,
returnType: n
};
try {
const e = await fetch(`${tA}/screenshot`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(r)
});
if (!e.ok) throw new Error(`Server responded with ${e.status}: ${e.statusText}`);
return await e.json()
} catch (e) {
throw console.error("Error fetching screenshot:", e), e
}
}
async function nA(e, t = 0, A = 0, n) {
const r = {
htmlChunks: e,
width: t,
height: A,
returnType: n
};
try {
const e = await fetch(`${tA}/screenshot`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(r)
});
if (!e.ok) throw new Error(`Server responded with ${e.status}: ${e.statusText}`);
return await e.json()
} catch (e) {
throw console.error("Error fetching chunked screenshot:", e), e
}
} function fA(e, t, A, n) {
e.innerHTML = '<div style="display:flex;height: 100%;align-items: center;justify-content: center;" class="exportgpt-loading-icon-shell bg-token-main-surface-secondary">\n<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="animate-spin exportgpt-loading-spinner exportgpt-loading-spinner-standalone" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><line x1="12" y1="2" x2="12" y2="6"></line><line x1="12" y1="18" x2="12" y2="22"></line><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line><line x1="2" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="22" y2="12"></line><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line></svg>\n</div>', async function(e) {
const t = qt();
if (!t || 0 === t.length) throw new Error("No message blocks found for screenshot export.");
const A = wA(t, e);
if (0 === A.length) throw new Error("No selected messages found for screenshot export.");
const n = function(e, t, A = qt()) {
if (!A || 0 === A.length) return [];
const n = wA(A, e);
if (n.length <= 1) return [];
const r = gA(A, n);
if (r.length <= 1) return [];
const i = function(e, t, A) {
const n = document.createElement("div"),
r = Tt();
n.style.position = "fixed", n.style.left = "-99999px", n.style.top = "0", n.style.width = A + "px", n.style.pointerEvents = "none", n.style.opacity = "0", n.style.zIndex = "-1", n.style.backgroundColor = r, n.classList.add("screenshot-main");
const i = [];
t.forEach(t => {
const A = e[t];
if (!A) return;
const o = A.cloneNode(!0);
o.style.backgroundColor = r, o.style.width = "100%", vA(o), Et(o), BA(o), yA(o), i.push({
index: t,
clone: o
}), n.appendChild(o)
}), document.body.appendChild(n);
const o = new Map;
return i.forEach(({
index: e,
clone: t
}) => {
o.set(e, Math.ceil(t.getBoundingClientRect().height))
}), n.remove(), o
}(A, n, t),
o = r.map(e => {
const t = e.indices.reduce((e, t) => e + (i.get(t) || 0), 0);
return {
...e,
measuredHeight: t,
estimatedHeight: t
}
});
if (o.reduce((e, t) => e + t.estimatedHeight, 0) <= 2e4) return [];
return function(e) {
const t = [];
let A = [],
n = 0;
e.forEach(e => {
const r = n + e.estimatedHeight,
i = r > 18e3,
o = r > 2e4;
A.length > 0 && (o || i && e.hasComplexContent) && (t.push(A), A = [], n = 0), A = A.concat(e.indices), n += e.estimatedHeight
}), A.length > 0 && t.push(A);
return t.length > 1 ? t : []
}(o)
}(e, rA, t),
r = (n.length > 0 ? n : [A]).map(e => function(e, t) {
const A = e.length,
n = function(e, t, A) {
const n = Ct("wrap", t, A);
return function(e, t, A, n) {
if (!A || A.length <= 65536) return [];
if (!e || 0 === e.length) return [];
if (!t || t.length <= 1) return [];
const r = gA(e, t);
if (r.length <= 1) return [];
const i = r.reduce((e, t) => e + t.byteSize, 0),
o = Math.max(0, A.length - i),
s = function(e, t = 0) {
const A = [];
let n = [],
r = 0,
i = 0;
e.forEach(e => {
const o = r + e.byteSize,
s = i + e.penalty,
a = t + o + s,
c = a > 32768,
l = a > 40960;
n.length > 0 && (l || c && e.hasComplexContent) && (A.push(n), n = [], r = 0, i = 0), n = n.concat(e.indices), r += e.byteSize, i += e.penalty
}), n.length > 0 && A.push(n);
return A
}(r, o);
if (s.length <= 1) return [];
return s.map(e => Ct("wrap", e, n)).filter(Boolean)
}(e, t, n, A)
}(e, t, A);
if (n.length > 0) return {
htmlChunks: n,
blockIndices: t
};
return {
html: Ct("wrap", t, A),
blockIndices: t
}
}(t, e)).filter(Boolean),
i = [];
for (const e of r) {
Be("getScreenshotImagesFromServer:plan", {
blockIndices: e.blockIndices,
htmlLength: e.html?.length || 0,
htmlChunkCount: e.htmlChunks?.length || 0,
htmlChunkLengths: e.htmlChunks?.map(e => e.length) || []
});
const t = e.html ? await pA(e.html) : e.html,
A = e.htmlChunks?.length > 0 ? await Promise.all(e.htmlChunks.map(pA)) : e.htmlChunks,
n = A?.length > 0 ? await nA(A, rA, iA, "url") : await AA(t, rA, iA, "url");
if (!n) throw new Error("null data");
if (n.error) throw new Error(n.error);
const r = n.url || (n.image ? "data:image/png;base64," + n.image : "");
if (!r) throw new Error("Screenshot response missing image url.");
Be("getScreenshotImagesFromServer:response", {
blockIndices: e.blockIndices,
hasUrl: Boolean(n.url),
hasInlineImage: Boolean(n.image),
srcPreview: r.slice(0, 120)
}), i.push({
src: r,
index: i.length,
blockIndices: e.blockIndices
})
}
return i
}(A.selectedItems).then(r => {
r && 0 !== r.length ? (A.screenshotItems = r, A.currentIndex = 0, A.modalIndex = 0, A.modalOpen = !1, t ? t(r) : SA(e, A)) : n("null data")
}).catch(e => {
n(e?.message || e)
})
}- snipcap.pro
Receives the screenshot-generation POST from the content script at /api/screenshot.