Is VPN add-on safe?

Medium risk

VPN add-on transmits a canvas and WebGL device fingerprint to api.uvpn.me on every sign-in, and sends account-linked telemetry to analytics.uvpn.me.

On sign-in and sign-up, the extension collects browser platform data, hardware concurrency, device memory, and canvas and WebGL fingerprints, hashes them, and sends the result as a persistent device identifier to api.uvpn.me regardless of user preferences. When the sendTechnicalData preference is enabled, it also POSTs account-linked analytics events — including a persistent user_id and session token — to analytics.uvpn.me. A hardcoded API bearer token for the analytics endpoint is embedded in the shipped bundle.

uVPN.mev7.1.6Firefox Add-ons
50Risk
Who publishes it

uVPN.me - no other listings under this identity, 5 shared hostnames

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
uVPN.me
Website

Shared hosts - 5 hostnames

Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.

analytics.uvpn.me
Also called by 2 other listings, including uVPN
api.uvpn.me
Also called by 2 other listings
geo.uvpn.me
Also called by 2 other listings, including uVPN
manage.uvpn.me
Also called by 2 other listings, including uVPN
uvpn.me
Also called by 2 other listings, including uVPN

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

Publishers can request a review.

Findings

Canvas and WebGL fingerprint sent during VPN login

MediumunwantedCWE-359Dynamic sandbox

Signing in to the VPN add-on computes a hashed identifier from canvas output, WebGL pixels, platform, processor count, device memory, and entered email.

What actually happens

You did this

You submit the VPN add-on sign-in form.

The same handler also runs when Enter submits the password field.

The extension did this

The extension computes a hashed uid from your browser, device, canvas, WebGL, and email signals.

That uid is added to the authentication request sent to the VPN service.

Fields used to create or transmit the login identifier

  • Hashed login identifier
    3095163482

    Gives the service a stable value tied to your browser and device traits during authentication.

  • Your email address
    alex.rivera@example.com

    Links the hashed identifier to the account you typed into the sign-in form.

  • Browser and device traits
    Linux x86_64, 8 cores, 8 GB device memory, touch support: false

    Adds details about the device and browser that can make the identifier more stable across sessions.

  • Canvas drawing input
    abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`~1!2@3#4$5%6^7&8*9(0)-_=+[{]}|;:',<.>/?

    Shows the fixed text pattern your browser renders before that canvas output becomes part of the uid.

  • WebGL pixel output
    [0,0,0,255,0,0,0,255,0,0,0,255]

    Captures graphics-rendering output from your browser, adding another device-specific signal.

Captured request

POSThttps://api.uvpn.me/v2/user/login

The code that does this

The shipped code builds the uid and merges it into the login POST

Readable version

Readable fingerprint and canvas/WebGL collection

deobfuscated/popup/popup.js
          }), function(t, e) {            var n = window.navigator,              r = n.platform,              s = n.hardwareConcurrency,              o = n.deviceMemory,              i = "ontouchstart" in window,              a = de(),              c = ge(),              u = JSON.stringify({                platform: r,                hardwareConcurrency: s,                deviceMemory: o,                touchSupport: i,                canvasFingerPrint: a,                webGlFingerPrint: c,                browserName: t,                email: e              });            return (0, Bt.Z)(fe(u) + "")          }),          de = function() {            try {              var t = document.createElement("canvas"),                e = t.getContext("2d");              return e.textBaseline = "top", e.font = "14px 'Arial'", e.textBaseline = "alphabetic", e.fillStyle = "#f60", e.fillRect(125, 1, 62, 20), e.fillStyle = "#069", e.fillText("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`~1!2@3#4$5%6^7&8*9(0)-_=+[{]}|;:',<.>/?", 2, 15), e.fillStyle = "rgba(102, 204, 0, 0.7)", e.fillText("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`~1!2@3#4$5%6^7&8*9(0)-_=+[{]}|;:',<.>/?", 4, 17), t.toDataURL()            } catch (t) {              return t            }          },          ge = function() {            var t, e;            (t = document.createElement("canvas")).width = 256, t.height = 128, e = t.getContext("webgl2") || t.getContext("experimental-webgl2") || t.getContext("webgl") || t.getContext("experimental-webgl") || t.getContext("moz-webgl");            try {              var n = e.createBuffer();              e.bindBuffer(e.ARRAY_BUFFER, n);              var r = new Float32Array([-.2, -.9, 0, .4, -.26, 0, 0, .7321, 0]);              e.bufferData(e.ARRAY_BUFFER, r, e.STATIC_DRAW), n.itemSize = 3, n.numItems = 3;              var s = e.createProgram(),                o = e.createShader(e.VERTEX_SHADER);              e.shaderSource(o, "attribute vec2 attrVertex;varying vec2 varyinTexCoordinate;uniform vec2 uniformOffset;void main(){varyinTexCoordinate=attrVertex+uniformOffset;gl_Position=vec4(attrVertex,0,1);}"), e.compileShader(o);              var i = e.createShader(e.FRAGMENT_SHADER);              e.shaderSource(i, "precision mediump float;varying vec2 varyinTexCoordinate;void main() {gl_FragColor=vec4(varyinTexCoordinate,0,1);}"), e.compileShader(i), e.attachShader(s, o), e.attachShader(s, i), e.linkProgram(s), e.useProgram(s), s.vertexPosAttrib = e.getAttribLocation(s, "attrVertex"), s.offsetUniform = e.getUniformLocation(s, "uniformOffset"), e.enableVertexAttribArray(s.vertexPosArray), e.vertexAttribPointer(s.vertexPosAttrib, n.itemSize, e.FLOAT, !1, 0, 0), e.uniform2f(s.offsetUniform, 1, 1), e.drawArrays(e.TRIANGLE_STRIP, 0, n.numItems)            } catch (t) {}            var a = new Uint8Array(131072);            return e.readPixels(0, 0, 256, 128, e.RGBA, e.UNSIGNED_BYTE, a), JSON.stringify(a).replace(/,?"[0-9]+":/g, "")          },

Readable sign-in handler passes uid to login

deobfuscated/popup/popup.js
            submitHandler: function() {              var t = this,                e = this.getSigninData(),                n = e.email,                r = e.password,                s = pe(this.getUserData("browser"), n);              this.setWaitingResponse(!0), ke({                uid: s,                email: n,                password: r              }).then((function(e) {                t.setWaitingResponse(!1), t.updateUserData(Ln(Ln({}, e), {}, {                  uid: s,                  email_added: !0,                  signedin: !0                })), t.updateUserAccountData(), t.$router.push("/")              })).catch((function(e) {                t.setWaitingResponse(!1), t.error = !0, t.setSystemMsg(e), console.log(e)              }))            },

Readable login function posts to Kt.user_login

deobfuscated/popup/popup.js
          ke = function(t) {            return new Promise(function() {              var e = (0, _e.Z)(Ht().mark((function e(n, r) {                var s, o, i, a, c, u, l, p;                return Ht().wrap((function(e) {                  for (;;) switch (e.prev = e.next) {                    case 0:                      if (s = zs.getters["user/getUserData"](), o = s.browser, i = s.platform, a = s.version, c = s.type, u = s.user_version, l = s.device_token, p = s.device_account_id, "connected" !== zs.getters["app/getProxyConnectionStatus"]) {                        e.next = 4;                        break                      }                      return e.next = 4, m().runtime.sendMessage({                        msg: "clearProxy"                      });                    case 4:                      fetch(ye(Kt.user_login), {                        method: "POST",                        headers: Se(Se({}, we), {}, {                          Authorization: zs.getters["user/getUserData"]("auth_token")                        }),                        body: JSON.stringify(Se({                          browser: o,                          platform: i,                          version: a,                          type: c,                          user_version: u,                          device_token: l,                          device_account_id: p                        }, t))                      }).then(Oe).then(function() {                        var t = (0, _e.Z)(Ht().mark((function t(e) {                          return Ht().wrap((function(t) {                            for (;;) switch (t.prev = t.next) {                              case 0:                                if ("success" !== e.type) {                                  t.next = 11;                                  break                                }                                return e.notify.length > 0 && zs.dispatch("app/setHomeMsg", e.notify), zs.dispatch("app/setSigninData", {                                  key: "password",                                  val: ""                                }), zs.dispatch("app/setSignupData", {                                  key: "pwd",                                  val: ""                                }), zs.dispatch("app/setSignupData", {                                  key: "pwdConfirm",                                  val: ""                                }), t.next = 7, zs.dispatch("user/updateUserData", {                                  device_account_id: e.data.id,                                  analyticsToken: null                                });                              case 7:                                Pe({                                  event: "evt_sign_in"                                }).then((function(t) {                                  var e = t.token;                                  zs.dispatch("user/updateUserData", {                                    analyticsToken: e                                  })                                })).catch((function(t) {                                  console.log(t)                                })), n(e.data), t.next = 12;                                break;                              case 11:                                r(e.msg);                              case 12:                              case "end":                                return t.stop()                            }                          }), t)                        })));                        return function(e) {                          return t.apply(this, arguments)                        }                      }()).catch((function(t) {                        return r(t)                      }));                    case 5:                    case "end":                      return e.stop()                  }                }), e)              })));              return function(t, n) {                return e.apply(this, arguments)              }            }())          },

Destination for the authentication request

    • api.uvpn.me

    Receives the VPN add-on login POST that includes the computed uid field.

What it can do

Permissions this extension asks for, as declared in version 7.1.6. 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

    *://*/*

  • Watch every request your browser makes

    webRequest

  • Watch, block and rewrite every request your browser makes

    webRequestBlocking

  • Route all of your browsing through a server of its choosing

    proxy

  • Store data in your browser

    storage

  • See the address and title of every tab you have open

    tabs

  • Schedule its own background tasks

    alarms

Where it sends data

Destinations our analysis observed VPN add-on 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.

  • analytics.uvpn.me

    VPN add-on sends data to analytics.uvpn.me. No other extension we have analyzed sends data here.

  • api.uvpn.me

    VPN add-on sends data to api.uvpn.me. No other extension we have analyzed sends data here.

Updated 2 November 2026amo-1483778