Is HTML to PDF safe?
HTML to PDF generates a device fingerprint on each popup open and transmits it with geolocation data to hub.malson.eu.
Each time the popup or side panel loads, the extension constructs a persistent identifier from WebGL, AudioContext, Canvas, CPU, language, and timezone data, then fetches the user's country from Cloudflare and POSTs the fingerprint along with it to hub.malson.eu. The extension also reads a tracking cookie from appstation.info to synchronize that identifier with the vendor's website. User interaction events (clicks on ratings, downloads, and settings) are additionally sent to Amplitude's analytics API using the same fingerprint as the user identifier.
Who publishes itrob384392black - 2 other listings from the same operator, none carrying a finding
rob384392black - 2 other listings 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
2 other listings published from this account, 40k+ 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
Device fingerprint and country sent on popup open
Opening the HTML to PDF popup or side panel creates or reuses a persistent device ID and sends it to hub.malson.eu with an IP-derived country, extension version, product name, and browser language, after fetching Cloudflare trace data.
What actually happens
You open the extension popup or side panel.
The extension prepares a persistent device identifier and posts it with country and language metadata to hub.malson.eu.
Captured request
- Content-Type
- application/json
{ "user_id": "device_well_x44cor0tw81783893116034", "product": "html_to_pdf", "country": "GB", "version": "3.4", "metadata": { "language": "en-US" }}Fields sent in the captured profile update
- Persistent device identifierdevice_well_x44cor0tw81783893116034
This lets the recipient recognize the same browser across later extension opens.
- IP-derived countryGB
This adds location context to the identifier without asking you to enter a location.
- Product namehtml_to_pdf
This tells the recipient which extension sent the update.
- Extension version3.4
This tells the recipient which installed version produced the request.
- Browser languageen-US
This adds a locale signal that can help distinguish or group users.
The code that does this
The bundled code creates the identifier and posts the profile update
Fingerprint generator
assets/js/db6554743727db43a2ce.js2796: (nn, en, an) => { "use strict"; an.d(en, { $: () => tn }); class tn { constructor() { this.fingerprint = null } async generate() { if (this.fingerprint) return this.fingerprint; try { const nn = [this.getWebGLFingerprint(), this.getAudioFingerprint(), this.getCanvasFingerprint(), this.getUserAgentFingerprint(), navigator.language, (new Date).getTimezoneOffset().toString()]; return this.fingerprint = this.simpleHash(nn.join("::")), `${this.fingerprint}` } catch (nn) { return `nofp${Date.now().toString()+Math.random().toString(36).substring(2,10)}` } } getWebGLFingerprint() { try { let nn = document.createElement("canvas"), en = nn.getContext("webgl") || nn.getContext("experimental-webgl"); if (!en) return "webgl_not_supported"; let an = en.getExtension("WEBGL_debug_renderer_info"); return an ? en.getParameter(an.UNMASKED_RENDERER_WEBGL) : "unknown" } catch (nn) { return "webgl_error" } } getAudioFingerprint() { try { if (!window.AudioContext && !window.webkitAudioContext) return "audiocontext_not_supported"; return (new(window.AudioContext || window.webkitAudioContext)).createAnalyser().frequencyBinCount.toString() } catch (nn) { return "audiocontext_error" } } getCanvasFingerprint() { try { let nn = document.createElement("canvas"), en = nn.getContext("2d"); return en ? (en.textBaseline = "top", en.font = "14px Arial", en.fillText("Hello, fingerprint!", 2, 2), nn.toDataURL()) : "canvas_not_supported" } catch (nn) { return "canvas_error" } } getUserAgentFingerprint() { try { let nn = navigator.userAgent, en = "Unknown OS"; return nn.includes("Windows") ? en = "Windows" : nn.includes("Macintosh") ? en = "Mac" : nn.includes("Linux") && (en = "Linux"), en + "::" + (nn.includes("Intel") ? "Intel" : nn.includes("ARM") ? "ARM" : "Other") + "::" + (nn.includes("Chrome") ? "Chrome" : nn.includes("Firefox") ? "Firefox" : "Other") } catch (nn) { return "ua_error" } } simpleHash(nn) { let en = 0, an = 0, tn = 0; for (let rn = 0; rn < nn.length; rn++) { let cn = nn.charCodeAt(rn); en = (en << 5) - en + cn, an = (an << 7) - an + cn, tn = (tn << 3) - tn + cn, en |= 0, an |= 0, tn |= 0 } return (Math.abs(en).toString(16) + Math.abs(an).toString(16) + Math.abs(tn).toString(16)).slice(0, 24) } }}Popup initialization and profile update
assets/js/db6554743727db43a2ce.jsclass bn { constructor() { this.userId = null } async init() { if (await cn.w.load_storageAsync(), this.userId = cn.w.get("user_id"), !this.userId) { const nn = await this.getDeviceIdFromWelcomePage(); this.userId = nn, nn || (this.userId = await this.createDeviceId()), cn.w.save("user_id", this.userId) } return this.userId } async getDeviceIdFromWelcomePage() { if (void 0 === chrome.cookies) return Promise.resolve(!1); const nn = "https://appstation.info/HTML_to_PDF.html"; return new Promise(((en, an) => { try { chrome.cookies.get({ url: nn, name: "html_to_pdf__userId" }, (nn => { if (chrome.runtime.lastError) return en(!1); en(!!nn && nn.value) })) } catch (tn) { en(!1) } })) } async createDeviceId() { const nn = new rn.$, en = await nn.generate(); return `device_app_${Math.random().toString(36).substring(2,10)}_${en}` } static getId() { return cn.w.get("user_id") } async getCountry() { try { const nn = await fetch("https://www.cloudflare.com/cdn-cgi/trace"), en = await nn.text(); return Object.fromEntries(en.trim().split("\n").map((nn => nn.split("=")))).loc } catch (nn) { return "null" } } getLang() { return navigator.language || navigator.languages?.[0] || "null" } async update() { const nn = await this.getCountry(), en = { user_id: this.userId, product: "html_to_pdf", country: nn, version: "3.4", metadata: { language: this.getLang() } }; try { (await fetch("https://hub.malson.eu/api/users/update", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(en) })).ok } catch (an) {} } static async trial() { const nn = { user_id: bn.getId(), product: "html_to_pdf" }; try { (await fetch("https://hub.malson.eu/api/users/trials/update", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(nn) })).ok } catch (en) {} }}const dn = new bn;await dn.init(), dn.update(), tn()External services involved in the update
- www.cloudflare.com
Returns trace text that includes the country code used in the update body.
- hub.malson.eu
Receives the profile update containing the persistent device identifier, country, product, version, and language.
What it can do
Permissions this extension asks for, as declared in version 3.4. 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 every site you visit
<all_urls>
Start, monitor and manage your downloads
downloads
See the address and title of every tab you have open
tabs
Run its own code inside the pages you visit
scripting
Act on the current tab, but only after you click the extension
activeTab
Store data in your browser
storage
Read and change cookies, including the ones that keep you signed in
cookies
Where it sends data
Destinations our analysis observed HTML 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.
- api2.amplitude.comwidely used
HTML to PDF sends data to api2.amplitude.com. A widely used service: 42 other extensions we have analyzed send data here.
- www.cloudflare.com
HTML to PDF sends data to www.cloudflare.com. 2 other extensions we have analyzed send data here.
- appstation.info
HTML to PDF sends data to appstation.info. No other extension we have analyzed sends data here.
- hub.malson.eu
HTML to PDF sends data to hub.malson.eu. No other extension we have analyzed sends data here.
