Is ChatGPT to PDF safe?

High risk

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.

ChatGPT to PDFv5.2.1Chrome Web Store
72Risk
Who publishes it

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.

Publisher
ChatGPT to PDF

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
What actually happens
You did this

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 did this

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.

JSON fields prepared for the conversion request
  • 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 styles
    body, main, article, div { max-width: 100% !important; } (illustrative)

    Carries styling from the ChatGPT page so the remote converter can reproduce the document layout.

  • Page title
    ChatGPT - Launch planning conversation (illustrative)

    Adds context about the ChatGPT page or custom document title attached to the export.

  • Export settings
    pageFormat=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.

Captured request
POSThttps://chatgptpdf.tech/api/convert-html-to-pdf-gpt

The code expects a successful response to be a PDF blob that it downloads in the page.

Headers
Content-Type
application/json
Captured request
POSThttps://chatgptpdf.tech/api/convert-html-to-doc-gpt

The code expects a successful response to be a DOC blob that it downloads in the page.

Headers
Content-Type
application/json
The code that does this

The export functions that clone ChatGPT content and post it for conversion

Readable version

Full-conversation PDF/DOC export request

contentScript.js
async 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.js
async 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);  }}
External conversion destination named in the code
    • 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.

Updated 30 September 2026dmlaldcdniomijdemigppfldkdahphib