Is 二维码小能手 safe?
二维码小能手 is medium risk. This QR-code extension's worker sends an encrypted POST with your fingerprint ID/version to api.qrstrategy.com, undisclosed in the listing. The body decrypts, with the extension's own key, to JSON with the fingerprint, past cookie clears.…
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Fingerprint & Version Sent to Undisclosed Server on Startup
This QR-code extension's worker sends an encrypted POST with your fingerprint ID/version to api.qrstrategy.com, undisclosed in the listing.
The body decrypts, with the extension's own key, to JSON with the fingerprint, past cookie clears.
You install this QR-code scanner/generator extension and browse normally.
No fingerprinting or data collection is mentioned in the extension's Chrome Web Store description.
On every service worker startup, the extension encrypts your browser fingerprint and extension version and sends them to api.qrstrategy.com.
api.qrstrategy.com is unrelated to any QR-code functionality and is contacted over plain HTTP.
| Field | Value | Why it matters | |
|---|---|---|---|
Browser fingerprint ID | 30fc5c92a3dca2ce3180a3684df381fb | A unique identifier from your browser and device. It persists across sessions and identifies your browser even after cookies are cleared. | |
Extension version | 0.0.6 | Tells the server exactly which build of the extension you're running. | |
Build flag | 1 | A fixed internal marker the extension always sends; likely used server-side to distinguish release channels. |
The request body leaves your browser as an unreadable block of ciphertext, but the AES key used to produce it is hardcoded inside the extension's own shipped code (see the code block below), so anyone who reads the extension's source can decrypt it just as easily as the destination server can.
{
"bf": 1,
"version": "0.0.6",
"fg": "30fc5c92a3dca2ce3180a3684df381fb"
}The startup request path in serviceworker.js
// state: cached fingerprint + server-provided config
const state = { fingerprint: "", jsonData: {} };
const sendFingerprintAndFetchConfig = (haveFingerprint) => {
if (!haveFingerprint) {
log("waiting", "fingerprint");
return;
}
log("starting", "init");
(async (fingerprint) => {
try {
const config = await (async (fingerprint) => {
log("outbound payload", { bf: BUILD_FLAG, version: getExtensionVersion(), fg: fingerprint });
const response = await fetch(`${C2_HOST}/sr105/qrinfo`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: encryptAesCbc(JSON.stringify({
bf: BUILD_FLAG,
version: getExtensionVersion(),
fg: fingerprint,
})),
});
const json = await response.json();
if (json.success === 1) {
const decrypted = decryptAesCbc(json.data);
return !!decrypted && JSON.parse(decrypted);
}
return false;
})(fingerprint);
log("received config", config);
state.jsonData = config;
new RedirectRuleManager(state.jsonData, state.fingerprint, BUILD_FLAG).init();
} catch {
log("request failed", "config fetch failed");
}
})(state.fingerprint);
};
// Content script posts { event: "setFingerprint", body: { id: <fingerprint> } }
// when no fingerprint is stored yet; background persists it and immediately
// fires the request above.
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.event === "getFingerprint") {
sendResponse(state.fingerprint ? { fingerprint: state.fingerprint } : false);
}
if (message.event === "setFingerprint") {
const fingerprintId = message?.body?.id;
log("received fingerprint", fingerprintId);
if (fingerprintId) {
chrome.storage.sync.set({ brext: fingerprintId });
state.fingerprint = fingerprintId;
sendFingerprintAndFetchConfig(true);
}
}
});
// On service-worker startup, reuse a fingerprint already persisted from a
// previous session.
chrome.storage.sync.get(["brext"], (stored) => {
if (stored.brext) {
log("cached fingerprint", stored.brext);
state.fingerprint = stored.brext;
sendFingerprintAndFetchConfig(true);
} else {
sendFingerprintAndFetchConfig(false);
}
});- api.qrstrategy.com
Receives the encrypted fingerprint ID and version at startup, returning an encrypted config payload. Not disclosed in the listing, which describes only QR scanning.
Browser Fingerprint Built On Every Page, Synced Across Devices
This QR-code extension loads FingerprintJS on every website you visit and derives a stable ID from your device and browser, then saves it to synced storage that follows you to every device on your Google account.
You browse to any website with the extension installed.
The extension's content script is injected on every page, matching *://*/*, whether or not you ever open the QR tool.
The content script computes a browser fingerprint using a bundled copy of the FingerprintJS library, without notifying you.
Nothing on the page indicates this is happening; there is no prompt, badge, or setting to opt out.
| Field | Value | Why it matters | |
|---|---|---|---|
Screen and display | 1920x1080, 24-bit color | Your screen resolution and color depth are combined with other traits into a value that rarely changes between visits. | |
Installed plugins | PDF Viewer, Native Client | The list of browser plugins and MIME types you have is uncommon enough to help single you out. | |
Hardware profile | deviceMemory: 8, hardwareConcurrency: 12 | Your device's memory size and CPU core count add further detail that narrows down which machine you're on. | |
Resulting fingerprint ID | 30fc5c92a3dca2ce3180a3684df381fb | All of the above are hashed into one ID that stays the same across sessions, even after you clear cookies. |
This is synced storage, not local-only. Chrome copies the ID to every device signed into the same Google account.
chrome.storage.sync, key 'brext'{
"brext": "30fc5c92a3dca2ce3180a3684df381fb"
}Fingerprint request/store glue code in contentscript.js
// ie = the FingerprintJS agent, already loading in the background
const fingerprintManager = new class {
constructor() {
this.fingerprint = "";
}
init() {
chrome.runtime.sendMessage({ event: "getFingerprint", body: {} }, (response) => {
if (response) {
// Service worker already has a fingerprint cached; reuse it.
const { fingerprint } = response;
this.fingerprint = fingerprint;
} else {
// No fingerprint yet: run FingerprintJS and compute one.
(async () => {
const agent = await fingerprintJsAgent;
const result = await agent.get();
const { visitorId } = await result;
return visitorId;
})().then((visitorId) => {
if (debugLoggingEnabled) {
console.log("%ccreated fingerprint", "color: #4e6ef2", visitorId);
}
chrome.runtime.sendMessage({ event: "setFingerprint", body: { id: visitorId } });
});
}
});
}
};
// Invoked at the bottom of the bundle on every page load:
// fingerprintManager.init();What it can do
Permissions this extension asks for, as declared in version 0.0.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
See which of your requests its blocking rules matched
declarativeNetRequestFeedback
Store data in your browser
storage
See the address and title of every tab you have open
tabs