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
45Risk

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

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
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.

02EvidenceNETWORK CAPTURE
Captured request
POSThttps://hub.malson.eu/api/users/update
Headers
Content-Typeapplication/json
Body
{
  "user_id": "device_well_x44cor0tw81783893116034",
  "product": "html_to_pdf",
  "country": "GB",
  "version": "3.4",
  "metadata": {
    "language": "en-US"
  }
}
03EvidenceFIELD TABLE
Fields sent in the captured profile update
FieldValueWhy it matters
Persistent device identifier
device_well_x44cor0tw81783893116034This lets the recipient recognize the same browser across later extension opens.
IP-derived country
GBThis adds location context to the identifier without asking you to enter a location.
Product name
html_to_pdfThis tells the recipient which extension sent the update.
Extension version
3.4This tells the recipient which installed version produced the request.
Browser language
en-USThis adds a locale signal that can help distinguish or group users.
04EvidenceCODE COMPARE
The code that does this

The bundled code creates the identifier and posts the profile update

What it actually does
Fingerprint generatorassets/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 updateassets/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()
05EvidenceTHIRD PARTY LIST
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.

Data recipients

hub.malson.euapi2.amplitude.comappstation.infowww.cloudflare.com
Updated 17 September 2026ljbonnllckbdpfpjncfaoiakjjgjfmde