Is VPN add-on safe?
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.
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
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.
You submit the VPN add-on sign-in form.
The same handler also runs when Enter submits the password field.
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.
| Field | Value | Why it matters | |
|---|---|---|---|
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. |
The shipped code builds the uid and merges it into the login POST
}), 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, "")
}, 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)
}))
}, 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)
}
}())
},- api.uvpn.me
Receives the VPN add-on login POST that includes the computed uid field.