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.
Who publishes ituVPN.me - no other listings under this identity, 5 shared hostnames
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.
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.
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.
What actually happens
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.
Fields used to create or transmit the login identifier
- Hashed login identifier3095163482
Gives the service a stable value tied to your browser and device traits during authentication.
- Your email addressalex.rivera@example.com
Links the hashed identifier to the account you typed into the sign-in form.
- Browser and device traitsLinux 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 inputabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`~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
The code that does this
The shipped code builds the uid and merges it into the login POST
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.
