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

Canvas and WebGL fingerprint sent during VPN login

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

The sign-in POST to api.uvpn.me sends this uid regardless of data-sharing preference.

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

02EvidenceFIELD TABLE
Fields used to create or transmit the login identifier
FieldValueWhy it matters
Hashed login identifier
3095163482Gives the service a stable value tied to your browser and device traits during authentication.
Your email address
alex.rivera@example.comLinks 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: falseAdds 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.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://api.uvpn.me/v2/user/login
04EvidenceCODE COMPARE
The code that does this

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

What it actually does
Readable fingerprint and canvas/WebGL collectiondeobfuscated/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 logindeobfuscated/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_logindeobfuscated/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)
              }
            }())
          },
05EvidenceTHIRD PARTY LIST
Destination for the authentication request
  • api.uvpn.me

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

Data recipients

api.uvpn.meanalytics.uvpn.me
Updated 17 September 2026amo-1483778