Is HTML to PDF safe?

Medium risk

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.

rob384392blackv3.4Chrome Web Store
49Risk
Who publishes it

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.

Publisher
rob384392black

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

MediumunwantedCWE-359Dynamic sandbox

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 did this

You open the extension popup or side panel.

The extension did this

The extension prepares a persistent device identifier and posts it with country and language metadata to hub.malson.eu.

Captured request

POSThttps://hub.malson.eu/api/users/update
Headers
Content-Type
application/json
Body
{  "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 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 code that does this

The bundled code creates the identifier and posts the profile update

Readable version

Fingerprint generator

assets/js/db6554743727db43a2ce.js
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)    }  }}

Popup initialization and profile update

assets/js/db6554743727db43a2ce.js
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()

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.

Updated 30 September 2026ljbonnllckbdpfpjncfaoiakjjgjfmde