Is Notion to PDF safe?

High risk

Notion to PDF is high risk. Clicking the export button reads Notion profile fields from the session, joins them with the page URL, base64-encodes it, sends it as type3 to api.notiontopdf.site/nftCurrency/getInfo. Testing saw the endpoint accept empty type3.…

windea.yuv1.3.0Chrome Web Store
77Risk
Who publishes it

windea.yu - 1 other listing from the same operator, none carrying a finding

What this publisher told the store about itself, and the other listings that told it the same thing.

Same store account

1 other listing published from this account, 10k+ users between them, none of them carrying a finding.

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

Publishers can request a review.

Findings

Notion Profile and Page URL Sent During Export

Clicking the export button reads Notion profile fields from the session, joins them with the page URL, base64-encodes it, sends it as type3 to api.notiontopdf.site/nftCurrency/getInfo.

Testing saw the endpoint accept empty type3.

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

You click the Notion to PDF export button on a Notion page.

The export workflow runs inside pages where the extension content script is installed.

The extension did this

The extension reads Notion profile fields, combines them with the page URL, and posts the encoded result to its server.

The profile read uses Notion session credentials already available in the browser.

Fields assembled into the type3 value
  • Current Notion page URL
    https://www.notion.so/acme/Project-Plan-4f2c8d9a7b6e4c0a9d1e2f3a4b5c6d7e (illustrative)

    This identifies which Notion page was open when you exported the PDF.

  • Notion email address
    alex@example.com (illustrative)

    This ties the export activity to the signed-in Notion account.

  • Notion display name
    Alex Example (illustrative)

    This adds the account holder name associated with the signed-in Notion profile.

  • Notion family and given names
    Example | Alex (illustrative)

    These profile fields can identify the person behind the Notion account.

Why you can't catch this in DevTools

The profile-bearing string is base64-encoded before it is placed into the type3 form field; this example is illustrative and generated with the shipped encoder.

Decoded value
https://www.notion.so/acme/Project-Plan-4f2c8d9a7b6e4c0a9d1e2f3a4b5c6d7e|alex@example.com|Alex Example|Example|Alex|
Captured request
POSThttps://api.notiontopdf.site/nftCurrency/getInfo

The endpoint accepted a POST during unauthenticated dynamic analysis; the recorded body contained an empty type3 value.

Headers
content-Type
application/x-www-form-urlencoded
Body
type3=+
The code that does this

Shipped code that reads Notion profile fields and sends type3

Readable version

Base64 encoder used before submission

deobfuscated/content.min.js
function a(e) {    return window.btoa(unescape(encodeURIComponent(e)))  }

Authenticated Notion API request helper

deobfuscated/content.min.js
async function B(e, t) {    const n = Object.assign({        credentials: "include"      }, t || {}),      o = function(e, t) {        const n = String(e || "").replace(/^\/+/, "");        return N(t) + "/api/v3/" + n      }(e),      i = "https://www.notion.so/api/v3/" + String(e || "").replace(/^\/+/, "");    try {      const e = await fetch(o, n);      return e.ok || o === i ? e : await fetch(i, n)    } catch (e) {      if (o !== i) return await fetch(i, n);      throw e    }  }

Form encoder for POST fields

deobfuscated/content.min.js
function O(e) {    return e = (e + "").toString(), encodeURIComponent(e).replace(/!/g, "%21").replace(/'/g, "%27").replace(/\(/g, "%28").replace(/\)/g, "%29").replace(/\*/g, "%2A").replace(/%20/g, "+")  }

POST builder for nftCurrency/getInfo

deobfuscated/content.min.js
function L(e, t, n, o, i) {    return new Promise((async (a, r) => {      var l = chrome.runtime.getManifest().version,        s = "FREE",        c = (await k.get("buyInfo")).buyInfo;      null != c && null != c.result && c.result.applicationId, null != c && ("Temp" == c.msg && (s = "TEMPPRO"), null != c.result && null != c.result.applicationId && (s = "PRO"));      var d = "id=" + O(e) + "&type1=" + O(t + " " + l + " " + s) + "&type2=" + O(n) + "&type3=" + O(o + " " + i) + "&type4=" + O(chrome.runtime.id);      fetch(T.URLgetInfo, {        method: "POST",        headers: {          "content-Type": "application/x-www-form-urlencoded"        },        body: d      }).then((e => {        e.json()      })).then((e => {        a(e)      })).catch((e => {        r(e)      }))    }))  }

Notion profile extraction from getSpaces

deobfuscated/content.min.js
async function q() {    try {      var e = await new Promise(((e, t) => {          B("getSpaces", {            method: "POST",            headers: {              pragma: "no-cache",              "cache-control": "no-cache"            }          }).then((e => e.json())).then((t => {            e(t)          })).catch((t => {            e("")          }))        })),        t = Object.keys(e),        n = "";      for (var o in t) {        var i = e[t[o]].notion_user[t[o]],          a = i.value.email,          r = i.value.name,          l = i.value.family_name,          s = i.value.given_name;        n = n + a + "|" + (void 0 === r ? "" : r) + "|" + (void 0 === l ? "" : l) + "|" + (void 0 === s ? "" : s) + "|"      }      return n    } catch (e) {      return "|" + e    }  }

PDF export click handler

deobfuscated/content.min.js
async function(e) {          if (U()) {            var t = !1,              n = !1;            try {              $("#buttonPDF").prop("value", chrome.i18n.getMessage("js_content_msg_exporting")), $("#buttonPDF").attr("disabled", !0);              var o = await q();              F.getExtensionId().then((e => {                L(e, "NTP", "Export", "Click", a(document.URL + "|" + o))              }));              var i = Date.now();              const e = await w("#buttonPDF", !1);              if (!e.success) return;              t = e.needsRestore, n = e.originalDarkMode;              var r = await k.get("paperType"),                l = await k.get("paperOrientation");              await async function(e, t) {                var n = !1,                  o = "#buttonPDF";                $(o).prop("value", chrome.i18n.getMessage("js_content_msg_exporting0of5"));                var i = document.getElementsByClassName("notion-scroller");                if (null != i)                  for (var a = 0; a < i.length; a++) {                    var r = i[a].parentNode;                    if (r && ("notion-frame" === r.className || r.classList.contains("notion-frame") || "main" === r.id)) {                      i[a].scrollTop = 0;                      break                    }                  }                var l = E(e, t),                  s = e + "_" + t + "_margin",                  c = await k.get(s, "LOCAL");                if (c[s]) {                  var d = c[s];                  l.PaperMarginLeft = d.left, l.PaperMarginRight = l.PaperWidth - d.right, l.PaperMarginTop = d.top, l.PaperMarginBottom = l.PaperHeight - d.bottom                }                l.PaperDisplayWidth = l.PaperWidth - l.PaperMarginLeft - l.PaperMarginRight, l.PaperDisplayHeight = l.PaperHeight - l.PaperMarginTop - l.PaperMarginBottom;                var p = {                    CanvasWidth: 1920,                    ContentWidth: 0,                    ContentHeight: 0,                    Ratio: 2,                    minWidth: 1480                  },                  g = {                    Zoom: 0,                    ImagePrintHeight: 0,                    ImagePrintWidth: 0,                    PDFFileName: "NotionToPDF.pdf",                    DebugExportOrginImage: !1,                    DebugExportCutImage: !1,                    DebugExportLineImage: !1,                    DebugMaskMode: !1                  };                let u = null,                  h = null;                u = document.getElementsByClassName("notion-frame")[0], h = u.cloneNode(!0), g.PDFFileName = D(n, h);                let m = document.querySelector("#notion-app > div > div:nth-child(1) > div > nav > div > div > div"),                  f = 0;                m && m.getBoundingClientRect && (f = Math.floor(m.getBoundingClientRect().width));                let y = 0;                try {                  u && u.getBoundingClientRect && (y = Math.floor(u.getBoundingClientRect().width))                } catch (e) {}!y && h && h.getBoundingClientRect && (y = Math.floor(h.getBoundingClientRect().width) || p.CanvasWidth), y || (y = p.CanvasWidth);                let v = document.createElement("div"),                  w = document.createElement("div");                w.setAttribute("style", `width: ${f}px; flex: 0 0 ${f}px;`), h.setAttribute("style", `width: ${y}px;`);                let b = f + y;                v.setAttribute("style", `display: flex; width: ${b}px;`), v.appendChild(w), v.appendChild(h), p.CanvasWidth = b, $(o).prop("value", chrome.i18n.getMessage("js_content_msg_exporting0of5")), setTimeout((function() {                  $(o).val() == chrome.i18n.getMessage("js_content_msg_exporting0of5") && $(o).prop("value", chrome.i18n.getMessage("js_content_msg_exporting1of5"))                }), 3e3), setTimeout((function() {                  $(o).val() == chrome.i18n.getMessage("js_content_msg_exporting1of5") && $(o).prop("value", chrome.i18n.getMessage("js_content_msg_exporting2of5"))                }), 9e3), setTimeout((function() {                  $(o).val() == chrome.i18n.getMessage("js_content_msg_exporting2of5") && $(o).prop("value", chrome.i18n.getMessage("js_content_msg_exporting3of5"))                }), 18e3);                var M = A(),                  _ = P();                _ && _.getBoundingClientRect();                var C = document.querySelector(".layout-content");                C && C.getBoundingClientRect().width, await x(l, p, g, _, n), R(M), $(o).prop("value", chrome.i18n.getMessage("js_content_msg_exporting3of5"))              }(r.paperType, l.paperOrientation);              var s = Date.now();              F.getExtensionId().then((e => {                L(e, "NTP", "Export", "End", s - i)              }))            } catch (e) {              H(e), U() && F.getExtensionId().then((t => {                L(t, "NTP", "Export", "Error", e)              }))            } finally {              $("#buttonPDF").attr("disabled", !1), "Small" == (await k.get("buttonSize")).buttonSize ? $("#buttonPDF").prop("value", chrome.i18n.getMessage("js_content_button_main_small")) : $("#buttonPDF").prop("value", chrome.i18n.getMessage("js_content_button_main")), await M(t, n)            }          } else j()        }
Hosts involved in the export telemetry path
    • www.notion.so

    Notion API host queried for workspace profile data using the signed-in browser session.

    • api.notiontopdf.site

    Notion to PDF service endpoint receiving the encoded type3 form field during export.

Reproduce it yourself

Decodes a Notion to PDF type3 form value so you can inspect the page URL and Notion profile fields that were encoded.

Requires
  • Node.js 18+
notiontopdf-type3-decode.js · js
#!/usr/bin/env nodeconst input = process.argv.slice(2).join(' ').trim();if (!input) {  console.error('Usage: node notiontopdf-type3-decode.js "type3=aHR0cHM6Ly93d3cubm90aW9uLnNvLw=="');  process.exit(1);}let value = input;if (input.includes('=')) {  const params = new URLSearchParams(input);  value = params.get('type3') || '';}if (!value) {  console.error('No type3 value found.');  process.exit(1);}const decoded = Buffer.from(value.replace(/ /g, '+'), 'base64').toString('utf8');console.log(decoded);
How to run it
  1. 1node notiontopdf-type3-decode.js "type3=aHR0cHM6Ly93d3cubm90aW9uLnNvLw=="

License checks send a persistent device ID every minute

Notion to PDF regularly contacts the developer's server to check license status.

Each check sends a persistent device tracking ID to api.notiontopdf.site; captured behavior shows this can repeat every minute while the browser runs.

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

The extension is installed or loaded in the browser.

The extension did this

The extension schedules a recurring license status check and sends its stored device tracking ID to the developer server.

Captured request
POSThttps://api.notiontopdf.site/nftCurrency/getBuyInfo
Body
id=NOTP2960850397D071A1
Fields in the request
  • Stored device tracking ID
    NOTP2960850397D071A1

    This persistent extension-stored value can link repeated license checks from the same browser installation.

Extension metadata sent with user action telemetry.

Using Notion to PDF, its telemetry sends the extension version, license tier, and Chrome runtime ID to api.notiontopdf.site/nftCurrency/getInfo.

The captured POST includes tab/action context plus stored subscription data like the FREE tier.

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

The user performs an extension action such as opening the popup or exporting.

The extension did this

The extension sends telemetry containing version, license tier, action context, and runtime ID.

Captured request
POSThttps://api.notiontopdf.site/nftCurrency/getInfo
Body
id=NOTP2960850397D071A1&type1=NTP+1.0.0+FREE&type2=POP&type3=+&type4=hfdhkamjdigajnbbbmfallhomnhpmken
Fields in the request
  • Tab or action context
    POP

    Shows which extension interaction generated the telemetry event.

  • Stored license status
    FREE

    Reports the subscription state stored by the extension, such as FREE, TEMPPRO, or PRO.

+1 more finding not shown

Updated 30 September 2026pionfefhjikbcffjfcfnjmmabfpblpma