Is Light QRcode safe?
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.…
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
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.
You open any website with Light QRcode installed.
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.
| Field | Value | Why it matters | |
|---|---|---|---|
Device fingerprint | 30fc5c92a3dca2ce3180a3684df381fb | A near-unique ID built from your browser and device signals, letting the same install be recognized across sessions. | |
Extension version | 2.6 | The installed version number of Light QRcode. | |
Config flag | 1 | A static flag the server uses to select which redirect-rule configuration to return. |
| Content-Type | application/json |
3Sr+57FtRT14Op7fUet7swoCdH9YhwBo9LK1FfA6JBNLTTOoJCFTECWK9z3/4kTMirsa44WsEkOx7CPZ4+Qsw1BUo0cDsSAWt6blqnfSLFM=
The POST body is encrypted, but the AES key ships in the same source file, so anyone observing the traffic can decrypt it.
{
"bf": 1,
"version": "2.6",
"fg": "30fc5c92a3dca2ce3180a3684df381fb"
}Fingerprint capture, the hardcoded key, and the POST that sends it out
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 } });
});
}
});
}
};// 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();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);
}
});- 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.
Decrypts a captured POST body sent to rsapi.qentifyrs.com using the AES key that ships in the extension's own source.
// 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
}
- 1Run npm install crypto-js.
- 2Capture the base64 POST body sent to rsapi.qentifyrs.com/sr105/qrinfo.
- 3Run: 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