Is ChatGPT to PDF safe?
ChatGPT to PDF sends ChatGPT conversation HTML and page metadata to chatgptpdf.tech when exporting PDFs or DOC files.
When a user exports a ChatGPT conversation, the extension posts the conversation HTML to chatgptpdf.tech for conversion. The request also includes embedded page styles, the page or custom title, page format and orientation, and other user-selected document options.
Who publishes itChatGPT to PDF - no other listings under this identity
ChatGPT to PDF - no other listings under this identity
What this publisher told the store about itself, and the other listings that told it the same thing.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
ChatGPT Exports Send Conversation HTML to chatgptpdf.tech
Clicking Save as PDF or DOC on a ChatGPT conversation clones the page, gathers styles and title metadata, and POSTs that JSON to chatgptpdf.tech.
Store text says "Privacy-First: conversions happen on your device," contradicting this upload.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You choose to export a ChatGPT conversation as a PDF or DOC file.
The same path is used for full-conversation exports and selected-message exports.
The extension packages conversation HTML, page styles, title metadata, and export settings for remote conversion.
The bundled store text says conversions happen on your device, but the code posts the package to chatgptpdf.tech.
- Conversation HTML<main><article data-testid="conversation-turn-2"><div data-message-author-role="user">Draft the launch plan.</div><div data-message-author-role="assistant">Here is a phased plan.</div></article></main> (illustrative)
Contains the ChatGPT conversation content you chose to export, including the surrounding page markup copied from the conversation area.
- Page stylesbody, main, article, div { max-width: 100% !important; } (illustrative)
Carries styling from the ChatGPT page so the remote converter can reproduce the document layout.
- Page titleChatGPT - Launch planning conversation (illustrative)
Adds context about the ChatGPT page or custom document title attached to the export.
- Export settingspageFormat=A4; pageOrientation=Portrait; removeLogo=false; toc=true; fontSize=16; lineSpacing=1.5; padding=20 (illustrative)
Options you selected for the PDF/DOC: page size, orientation, logo removal, table of contents, font size, line spacing, padding, watermark.
The code expects a successful response to be a PDF blob that it downloads in the page.
- Content-Type
- application/json
The code expects a successful response to be a DOC blob that it downloads in the page.
- Content-Type
- application/json
The export functions that clone ChatGPT content and post it for conversion
Full-conversation PDF/DOC export request
contentScript.jsasync function generatePDF() { await initializePageSize(); const button = document.getElementById("save-html-button"); if (!button) return; showLoadingIndicator(button); const modifiedBody = removeUnwantedElements(); const mainElement = modifiedBody.querySelector("main"); if (!mainElement) { alert("Main element not found."); resetButton(button); return; } const styles = await getEmbeddedStyles(); const shadowStyles = await getShadowDOMStyles(); const customStyles = ` body, main, article, div{ max-width: 100% !important; } `; const allStyles = `${styles}\n${shadowStyles}\n${customStyles}`; let htmlContent = mainElement.outerHTML; htmlContent = htmlContent.replace(/' /g, "'"); htmlContent = removeButtons(htmlContent, toRemoveLogo); // --- Determine title for API payload --- const titleElement = document.querySelector("head title"); const pageTitle = titleElement ? titleElement.textContent.trim() : "Document"; let titleForAPI = ""; if (useCustomTitle && customTitle.trim().length > 0) { titleForAPI = customTitle; } else if (addTitle) { titleForAPI = pageTitle; } // --- End title logic --- const currPageOrientation = landscapeMode ? "Landscape" : "Portrait"; try { // Determine the API endpoint based on the selected output format const endpointUrl = outputFormat === "pdf" ? "https://chatgptpdf.tech/api/convert-html-to-pdf-gpt" : "https://chatgptpdf.tech/api/convert-html-to-doc-gpt"; const apiResponse = await fetch(endpointUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ html: htmlContent, pageFormat: currentPageSize, pageOrientation: currPageOrientation, allStyles, onlyAiResponseNeeded: onlyChatGPTResponses, title: titleForAPI, removeLogo: toRemoveLogo, toc: addTableOfContent, fontSize: fontSize, lineSpacing: lineSpacing, padding: padding, watermark: customWatermark, customWatermarkSize, customWatermarkColor, }), }); if (apiResponse.ok) { const blob = await apiResponse.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; const pdfFileName = useCustomTitle && customTitle.trim().length > 0 ? customTitle : pageTitle; a.download = `${pdfFileName}.${outputFormat === "pdf" ? "pdf" : "doc"}`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } else { throw new Error("Tip : For large files sometimes you need to Try Again."); } } catch (error) { console.error("Error during PDF generation:", error); alert(`An error occurred: ${error.message}`); } finally { resetButton(button); }}Selected-message export request
contentScript.js async generateSelectedPDF() { if (this.selectedArticles.size === 0 && this.selectedChildren.size === 0) { alert("Please select at least one message."); return; } this.confirmButton.disabled = true; this.confirmButton.textContent = "Saving..."; this.confirmButton.style.backgroundColor = "#212121"; this.confirmButton.style.cursor = "not-allowed"; this.confirmButton.style.color = "#4CAF50"; this.toggleModeButton.disabled = true; this.toggleModeButton.style.backgroundColor = "#A9A9A9"; this.toggleModeButton.style.cursor = "not-allowed"; this.toggleModeButton.style.color = "white"; let selectedHTML = ""; this.processedArticles = new Set(); const combinedSet = new Set([...this.selectedArticles, ...this.selectedChildren]); combinedSet.forEach((element) => { if (this.excludedElements.has(element)) return; const hasArticleAncestor = [element, element.parentElement, element.parentElement?.parentElement].some((el) => el && el.tagName.toLowerCase() === "article"); if (hasArticleAncestor) { const existingArticle = [element, element.parentElement, element.parentElement?.parentElement].find((el) => el && el.tagName.toLowerCase() === "article"); if (existingArticle && !this.processedArticles.has(existingArticle)) { const clonedArticle = existingArticle.cloneNode(true); selectedHTML += clonedArticle.outerHTML; this.processedArticles.add(existingArticle); } return; } else { const parent1 = element.parentElement; const parent2 = parent1 ? parent1.parentElement : null; if (!parent1 || !parent2) { const articleWrapper = document.createElement("article"); if (parent1) { const clonedParent1 = parent1.cloneNode(false); const clonedElement = element.cloneNode(true); clonedElement.classList.remove("message-selector-selected"); clonedParent1.appendChild(clonedElement); articleWrapper.appendChild(clonedParent1); } else { const clonedElement = element.cloneNode(true); clonedElement.classList.remove("message-selector-selected"); articleWrapper.appendChild(clonedElement); } const articleId = element.getAttribute("id") || `article-${Math.random().toString(36).substring(2, 9)}`; articleWrapper.id = articleId; selectedHTML += articleWrapper.outerHTML; } else { const clonedParent2 = parent2.cloneNode(false); const clonedParent1 = parent1.cloneNode(false); const clonedElement = element.cloneNode(true); clonedElement.classList.remove("message-selector-selected"); clonedParent1.appendChild(clonedElement); clonedParent2.appendChild(clonedParent1); const articleWrapper = document.createElement("article"); const articleId = clonedElement.getAttribute("id") || `article-${Math.random().toString(36).substring(2, 9)}`; articleWrapper.id = articleId; articleWrapper.appendChild(clonedParent2); selectedHTML += articleWrapper.outerHTML; } } }); const sanitizedHTML = this.sanitizeSelectedHTML(selectedHTML); const finalHTML = ` <html> <head> <style> body { padding-left: 20px; font-family: Arial, sans-serif; } ul, ol { padding-left: 20px; } </style> </head> <body> ${sanitizedHTML} </body> </html> `; try { // Get the styles from the page const styles = await getEmbeddedStyles(); const shadowStyles = await getShadowDOMStyles(); const customStyles = ` body, main, article, div{ max-width: 100% !important; } `; const allStyles = `${styles}\n${shadowStyles}\n${customStyles}`; // Determine API endpoint based on output format const endpointUrl = outputFormat === "pdf" ? "https://chatgptpdf.tech/api/convert-html-to-pdf-gpt" : "https://chatgptpdf.tech/api/convert-html-to-doc-gpt"; const apiResponse = await fetch(endpointUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ html: finalHTML, pageFormat: currentPageSize, pageOrientation: landscapeMode ? "Landscape" : "Portrait", allStyles, // Now passing the styles properly onlyAiResponseNeeded: outputFormat === "pdf" ? false : onlyChatGPTResponses, title: useCustomTitle && customTitle.trim().length > 0 ? customTitle : document.querySelector("head title")?.textContent.trim() || "Document", removeLogo: toRemoveLogo, toc: addTableOfContent, fontSize: fontSize, lineSpacing: lineSpacing, padding: padding, watermark: customWatermark, customWatermarkSize, customWatermarkColor, }), }); if (apiResponse.ok) { const blob = await apiResponse.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${useCustomTitle && customTitle.trim().length > 0 ? customTitle : document.querySelector("head title") ? document.querySelector("head title").textContent.trim() : "Document"}.${outputFormat === "pdf" ? "pdf" : "doc"}`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.cleanup(); activeMessageSelector = null; } else { throw new Error("Tip : For large files sometimes you need to Try Again."); } } catch (error) { console.error("Error during PDF generation:", error); alert(`An error occurred: ${error.message}`); } finally { this.confirmButton.disabled = false; this.confirmButton.textContent = `Save Selected as ${outputFormat.toUpperCase()}`; // Changed to dynamic format this.confirmButton.style.backgroundColor = "#6CB4EE"; this.confirmButton.style.cursor = "pointer"; } }Full-conversation DOC export request
contentScript.jsasync function generateDOC() { await initializePageSize(); // Use the common save button's ID, not a nonexistent "save-doc-button" const button = document.getElementById("save-html-button"); if (!button) return; showLoadingIndicator(button); const modifiedBody = removeUnwantedElements(); const mainElement = modifiedBody.querySelector("main"); if (!mainElement) { alert("Main element not found."); resetButton(button); return; } const styles = await getEmbeddedStyles(); const shadowStyles = await getShadowDOMStyles(); const allStyles = `${styles}\n${shadowStyles}`; let htmlContent = mainElement.outerHTML; htmlContent = htmlContent.replace(/' /g, "'"); htmlContent = removeButtons(htmlContent, toRemoveLogo); // --- Determine title for API payload --- const titleElement = document.querySelector("head title"); const pageTitle = titleElement ? titleElement.textContent.trim() : "Document"; let titleForAPI = ""; if (useCustomTitle && customTitle.trim().length > 0) { titleForAPI = customTitle; } else if (addTitle) { titleForAPI = pageTitle; } // --- End title logic --- const currPageOrientation = landscapeMode ? "Landscape" : "Portrait"; try { // Removed extra space at the beginning of the URL: const apiResponse = await fetch("https://chatgptpdf.tech/api/convert-html-to-doc-gpt", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ html: htmlContent, pageFormat: currentPageSize, pageOrientation: currPageOrientation, allStyles, onlyAiResponseNeeded: onlyChatGPTResponses, title: titleForAPI, removeLogo: toRemoveLogo, toc: addTableOfContent, fontSize: fontSize, lineSpacing: lineSpacing, padding: padding, watermark: customWatermark, customWatermarkSize, customWatermarkColor, }), }); if (apiResponse.ok) { const docBlob = await apiResponse.blob(); const docUrl = URL.createObjectURL(docBlob); const docLink = document.createElement("a"); docLink.href = docUrl; const docFileName = useCustomTitle && customTitle.trim().length > 0 ? customTitle : pageTitle; docLink.download = `${docFileName}.doc`; document.body.appendChild(docLink); docLink.click(); document.body.removeChild(docLink); URL.revokeObjectURL(docUrl); } else { throw new Error("Tip: For large files sometimes you need to try again."); } } catch (error) { console.error("Error during DOC generation:", error); alert(`An error occurred: ${error.message}`); } finally { resetButton(button); }}- chatgptpdf.tech
Receives the JSON conversion request containing ChatGPT conversation HTML, page styles, page title, and export options, then returns a generated PDF or DOC blob.
What it can do
Permissions this extension asks for, as declared in version 5.2.1. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.
Store data in your browser
storage
Where it sends data
Destinations our analysis observed ChatGPT to PDF 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.
- chatgptpdf.tech
ChatGPT to PDF sends data to chatgptpdf.tech. No other extension we have analysed sends data here.