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.
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.
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.
| Content-Type | application/json |
{
"user_id": "device_well_x44cor0tw81783893116034",
"product": "html_to_pdf",
"country": "GB",
"version": "3.4",
"metadata": {
"language": "en-US"
}
}| Field | Value | Why it matters | |
|---|---|---|---|
Persistent device identifier | device_well_x44cor0tw81783893116034 | This lets the recipient recognize the same browser across later extension opens. | |
IP-derived country | GB | This adds location context to the identifier without asking you to enter a location. | |
Product name | html_to_pdf | This tells the recipient which extension sent the update. | |
Extension version | 3.4 | This tells the recipient which installed version produced the request. | |
Browser language | en-US | This adds a locale signal that can help distinguish or group users. |
The bundled code creates the identifier and posts the profile update
2796: (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)
}
}
}class 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()- 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.