Is Light QRcode safe?

High risk

Light QRcode is high risk. Light QRcode runs a fingerprinting library on every page you visit and sends the fingerprint to rsapi.qentifyrs.com. We captured and decrypted the request; the body held the fingerprint and extension version.…

nslirui8v2.6Chrome Web Store
75Risk

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

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

Light QRcode fingerprints your device and sends it to a third-party host

Light QRcode runs a fingerprinting library on every page you visit and sends the fingerprint to rsapi.qentifyrs.com.

We captured and decrypted the request; the body held the fingerprint and extension version.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You open any website with Light QRcode installed.

The extension did this

The extension builds a device fingerprint and sends it to a third-party server over an encrypted connection.

The fingerprint is generated once and reused on later service worker restarts.

02EvidenceFIELD TABLE
What the encrypted payload contains
FieldValueWhy it matters
Device fingerprint
30fc5c92a3dca2ce3180a3684df381fbA near-unique ID built from your browser and device signals, letting the same install be recognized across sessions.
Extension version
2.6The installed version number of Light QRcode.
Config flag
1A static flag the server uses to select which redirect-rule configuration to return.
03EvidenceNETWORK CAPTURE
Captured request
POSThttp://rsapi.qentifyrs.com/sr105/qrinfo
The server replies with {success:1,data:<ciphertext>}. The data field decrypts to a redirect-rule configuration.
Headers
Content-Typeapplication/json
Body
3Sr+57FtRT14Op7fUet7swoCdH9YhwBo9LK1FfA6JBNLTTOoJCFTECWK9z3/4kTMirsa44WsEkOx7CPZ4+Qsw1BUo0cDsSAWt6blqnfSLFM=
04EvidenceOPAQUE REVEAL
Why you can't catch this in DevTools

The POST body is encrypted, but the AES key ships in the same source file, so anyone observing the traffic can decrypt it.

What's actually being sent
{
  "bf": 1,
  "version": "2.6",
  "fg": "30fc5c92a3dca2ce3180a3684df381fb"
}
05EvidenceCODE COMPARE
The code that does this

Fingerprint capture, the hardcoded key, and the POST that sends it out

What it actually does
contentscript.js, computes the fingerprint and hands it to the workerjs/contentscript.js
const fingerprintCollector = new class {
  constructor() {
    this.fingerprint = "";
  }
  init() {
    // Ask the service worker whether a fingerprint is already cached
    chrome.runtime.sendMessage({ event: "getFingerprint", body: {} }, (cached) => {
      if (cached) {
        this.fingerprint = cached.fingerprint;
      } else {
        // No cached value yet: run FingerprintJS and report the new visitorId
        (async () => {
          const fpAgent = await fpPromise;
          const result = await fpAgent.get();
          const { visitorId } = await result;
          return visitorId;
        })().then((visitorId) => {
          chrome.runtime.sendMessage({ event: "setFingerprint", body: { id: visitorId } });
        });
      }
    });
  }
};
serviceworker.js, hardcoded AES key and destination hostserviceworker.js
// Hardcoded encryption key and destination host
const AES_KEY = "D96C445CAB84B110";   // reused as the IV
const DEBUG_LOGGING = false;
const CONFIG_HOST = "http://rsapi.qentifyrs.com";
const debugLog = (label, data) => {
  if (DEBUG_LOGGING) console.log(`%c${label}`, "color: #4e6ef2", data);
};

const getExtensionVersion = () => chrome.runtime.getManifest().version;

const encryptPayload = (plaintext, key = AES_KEY) =>
  CryptoJS.AES.encrypt(plaintext, CryptoJS.enc.Utf8.parse(key), {
    iv: CryptoJS.enc.Utf8.parse(key),
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  }).toString();
serviceworker.js, encrypts and POSTs the fingerprintserviceworker.js
const state = { fingerprint: "", jsonData: {} };

const fetchConfig = (haveFingerprint) => {
  if (!haveFingerprint) {
    debugLog("waiting for fingerprint", "fingerprint");
    return;
  }
  debugLog("starting config fetch", "init");
  (async (fingerprint) => {
    try {
      const config = await (async (fp) => {
        debugLog("outbound payload", { bf: 1, version: getExtensionVersion(), fg: fp });
        const res = await fetch(`${CONFIG_HOST}/sr105/qrinfo`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: encryptPayload(JSON.stringify({ bf: 1, version: getExtensionVersion(), fg: fp })),
        });
        const parsed = await res.json();
        if (parsed.success === 1) {
          const decrypted = decryptPayload(parsed.data);
          return !!decrypted && JSON.parse(decrypted);
        }
        return false;
      })(fingerprint);
      debugLog("redirect-rule config", config);
      state.jsonData = config;
      new RuleWriter(state.jsonData, state.fingerprint, 1).init();
    } catch {
      debugLog("config fetch failed", "config request failed");
    }
  })(state.fingerprint);
};

// The content script hands the fingerprint over here
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.event === "getFingerprint") {
    sendResponse(state.fingerprint ? { fingerprint: state.fingerprint } : false);
  }
  if (message.event === "setFingerprint") {
    const id = message?.body?.id;
    debugLog("fingerprint received", id);
    if (id) {
      chrome.storage.local.set({ brext: id });
      state.fingerprint = id;
      fetchConfig(true);
    }
  }
});

// On service worker startup, reuse whatever fingerprint is already cached
chrome.storage.local.get(["brext"], (stored) => {
  if (stored.brext) {
    debugLog("cached fingerprint", stored.brext);
    state.fingerprint = stored.brext;
    fetchConfig(true);
  } else {
    fetchConfig(false);
  }
});
06EvidenceTHIRD PARTY LIST
Where the fingerprint goes
  • rsapi.qentifyrs.com

    Receives the encrypted fingerprint and extension version on every install and worker restart. Returns a redirect-rule configuration. No privacy policy was found for this domain.

07EvidenceARTIFACT
Reproduce it yourself

Decrypts a captured POST body sent to rsapi.qentifyrs.com using the AES key that ships in the extension's own source.

RequiresNode.js 18+npm install crypto-js
decrypt_qentifyrs_payload.js · js
// decrypt_qentifyrs_payload.js
// Decrypts a POST body captured going to rsapi.qentifyrs.com/sr105/qrinfo.
// The AES key is hardcoded in Light QRcode's serviceworker.js and reused as
// the IV, so no extension install is needed to decrypt a captured body.

const CryptoJS = require('crypto-js');

const KEY = 'D96C445CAB84B110';
const IV = KEY;

function decrypt(base64Body) {
  const bytes = CryptoJS.AES.decrypt(base64Body, CryptoJS.enc.Utf8.parse(KEY), {
    iv: CryptoJS.enc.Utf8.parse(IV),
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  });
  return bytes.toString(CryptoJS.enc.Utf8);
}

const input = process.argv[2];
if (!input) {
  console.error('Usage: node decrypt_qentifyrs_payload.js <base64_body>');
  process.exit(1);
}

const plaintext = decrypt(input);
console.log('Decrypted:', plaintext);
try {
  console.log('Parsed JSON:', JSON.stringify(JSON.parse(plaintext), null, 2));
} catch {
  // not JSON, raw plaintext already printed above
}
How to run it
  1. 1
    Run npm install crypto-js.
  2. 2
    Capture the base64 POST body sent to rsapi.qentifyrs.com/sr105/qrinfo.
  3. 3
    Run: node decrypt_qentifyrs_payload.js <base64_body>

What it can do

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

    *://*/*

  • Block and redirect the requests your browser makes

    declarativeNetRequest

  • Store data in your browser

    storage

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

    tabs

Updated 30 September 2026pmpklfpmdhjefcdgdajeplahennjhecf