Is ChatGPT to PDF safe?

High risk

ChatGPT to PDF is high risk. ChatGPT PDF export's remote-generation path builds a JSON payload from page HTML, message, title, settings, user info, then POSTs to chatgpt-to-pdf-nodejs-production.up.railway.app. No live request was captured; evidence is the code path.…

chatgpt2pdfv1.3.12Chrome Web Store
75Risk

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

ChatGPT PDF exports can post conversation HTML to Railway

ChatGPT PDF export's remote-generation path builds a JSON payload from page HTML, message, title, settings, user info, then POSTs to chatgpt-to-pdf-nodejs-production.up.railway.app.

No live request was captured; evidence is the code path.

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

You start the extension PDF export on a ChatGPT conversation.

The remote request path is used for qualifying exports where the generated HTML is below the code threshold and does not match the image condition.

The extension did this

The extension packages the conversation content and account context for a remote PDF-generation request.

The background script sends the JSON to the Railway-hosted PDF service with an extension-generated authorization token.

Fields the source code places in the remote PDF request
  • Conversation title
    Q3 incident response planning (illustrative)

    Names the ChatGPT conversation being exported.

  • Conversation HTML
    <div class="group/turn-messages">Discuss the SSO rollout timeline</div> (illustrative)

    Contains the rendered ChatGPT conversation content and page formatting selected for the PDF.

  • Message text records
    textdoc-message-0: <div class="cm-line">Roll out to admins first</div> (illustrative)

    Includes text-document message fragments collected from the ChatGPT page.

  • User information
    patricia.nguyen@example.com, paid=true (illustrative)

    Can attach account identifiers and an email address to the exported conversation.

  • Export metrics
    pdfPrintType=full, font=default, fontSize=normal (from code fields)

    Records how the PDF was produced and which display settings were selected.

Captured request
POSThttps://chatgpt-to-pdf-nodejs-production.up.railway.app/generate-pdf-from-html

No live response was captured during dynamic analysis; the source code constructs this POST when the remote PDF path is used.

The code that does this

The content script builds the export data and the background script posts it

Readable version

Readable background POST construction

deobfuscated/background.js
      }(bo().m(function t(e, r, i, n) {        var s, a, o, u, l, h, c, d, f, m, _, y;        return bo().w(function(t) {          for (;;) switch (t.n) {            case 0:              return t.n = 1, b();            case 1:              return s = t.v, a = chrome.i18n.getUILanguage(), t.n = 2, v();            case 2:              return _o(o = t.v.toString(), "startFetchPdf", {                paywallUserId: i.id,                email: i.email              }), u = performance.now(), t.n = 3, fetch("https://chatgpt-to-pdf-nodejs-production.up.railway.app/generate-pdf-from-html", {                method: "POST",                headers: {                  "Content-Type": "application/json",                  Authorization: "Bearer " + s,                  "accept-Language": a                },                body: JSON.stringify({                  title: e.title,                  html: e.html,                  textDocMessages: e.textDocMessages,                  theme: r.theme,                  font: r.font,                  fontSize: r.fontSize,                  showWatermark: r.showWatermark,                  userInfo: i,                  metricInfo: n                })              });            case 3:              if (l = t.v, h = performance.now() - u, _o(o, "endFetchPdf", {                  code: l.status,                  paywallUserId: i.id,                  email: i.email,                  latency: h                }), l.ok) {                t.n = 5;                break              }              return t.n = 4, l.text();            case 4:              throw c = t.v, 404 === l.status ? (C(), c = "The requested server was not found. Please try again later.") : l.status >= 500 && (C(), c = "There's a problem with our server. Please try again later."), new Error(c);            case 5:              return d = Eo(e.title), f = "".concat(d, ".pdf"), t.n = 6, l.blob();            case 6:              return m = t.v, t.n = 7, p(m);            case 7:              return _ = t.v, y = "data:application/pdf;base64,".concat(_), t.n = 8, g(y, f);            case 8:              I();            case 9:              return t.a(2)          }        }, t)      })), wo.apply(this, arguments)

Readable content-script export payload

deobfuscated/contentScript.js
                      return e.n = 18, (0, s.B)(t, i);                    case 18:                      p = e.v, f = p.title, g = p.html, v = p.textDocMessages, _ = "full", i && (_ = "selected"), t && (_ = "aiTextOnly"), m = {                        title: f,                        html: g,                        textDocMessages: Object.fromEntries(v)                      }, y = {                        theme: F,                        font: O,                        fontSize: L,                        showWatermark: yh                      }, b = 0, w = vh(v.values());                      try {                        for (w.s(); !(x = w.n()).done;) {                          k = x.value, S = vh(k);                          try {                            for (S.s(); !(P = S.n()).done;) T = P.value, b += T.length                          } catch (e) {                            S.e(e)                          } finally {                            S.f()                          }                        }                      } catch (e) {                        w.e(e)                      } finally {                        w.f()                      }                      if (!(Math.floor((g.length + f.length + b) / 1024) < 2e3) || (0, s.M)(g)) {                        e.n = 19;                        break                      }                      chrome.runtime.sendMessage({                        action: r.bL,                        data: {                          contentData: m,                          params: y,                          userInfo: bh,                          metricInfo: {                            pdfPrintType: _,                            font: O,                            fontSize: L                          }                        }                      }, function(e) {                        A(), e && e.error ? (0, n.D)(e.error) : (R && z(), o(function(e) {                          e && l()                        }))                      }), e.n = 23;
Remote service receiving the export request
    • chatgpt-to-pdf-nodejs-production.up.railway.app

    Railway-hosted PDF generation endpoint that receives the export JSON built from the ChatGPT page.

PostHog Analytics Sends User Email and Subscription Status on Every PDF Export

Each time you export a ChatGPT conversation to PDF, the extension sends your registered email, account ID, and subscription tier to PostHog's EU analytics servers, before and after generation, tying your identity to every export.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You export a ChatGPT conversation to PDF.

The export action is the sole trigger, no additional interaction is required.

The extension did this

The extension sends your email address and account ID to PostHog analytics on eu.i.posthog.com, both before and after the PDF generation request completes.

The 'startFetchPdf' event carries your email and paywall user ID. The 'endFetchPdf' event adds the HTTP status code and request latency.

The code that does this

PostHog initialization and identity binding (background.js, lines 16465-16470, 16728-16729)

Readable version
// background.js lines 16465–16470function sendAnalyticsEvent(userId, eventName, properties) {  identifyUser(userId);  posthog.capture(eventName, properties);}function identifyUser(userId) {  posthog.identify(userId); // links all future events to this user's person profile}// background.js lines 16728–16729posthog.init('phc_QsGf4ScUul0ZSUq6uWGb3xOLtegD6pGD4rTI0Scz8B8', {  api_host: 'https://eu.i.posthog.com'});
Fields transmitted in each export event
  • Your email address
    jane.smith@example.com

    Your registered account email is sent with each export event, permanently linking the action to your identity in the developer's analytics.

  • Paywall user ID
    usr_7f3a29bc1d44e8a2

    A persistent ID tied to your subscription account, used as the PostHog person identifier across all sessions.

  • Event name
    endFetchPdf

    Labels the action you took ('startFetchPdf' or 'endFetchPdf'), building a behavioral timeline in the developer's PostHog dashboard.

  • Request latency
    1823

    The time in milliseconds the PDF server took to respond, included in the end event.

  • HTTP status code
    200

    The server response code, indicating whether the export succeeded or failed.

Analytics destination
    • eu.i.posthog.com

    PostHog analytics (EU region). Receives user email, account ID, and export event data. Retention is subject to PostHog's and the developer's data policies.

What it can do

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

    *://chatgpt.com/*

  • Act on the current tab, but only after you click the extension

    activeTab

  • Start, monitor and manage your downloads

    downloads

  • Store data in your browser

    storage

Updated 21 September 2026hiiildgldbpfbegcfgemoliikibfhaeh