Is Librezam safe?
Librezam sends audio fingerprint samples to an uncontrolled third-party Deno Deploy proxy and embeds recoverable API credentials in its bundle.
When identifying music via the Netease backend, the extension POSTs raw PCM audio data to a Deno Deploy endpoint controlled by a third party (foxrefire), not the extension author or Netease. Separately, ACRCloud API access keys are stored as AES-CBC blobs whose decryption key is derived entirely from bundle-visible material — the extension name and a hardcoded UUID — making them recoverable by anyone with access to the extension files.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Netease recognition posts audio to a Deno Deploy proxy
Using Librezam's music recognition with the Netease backend turns captured tab or mic audio into mono PCM and posts it to a Deno Deploy host, not a Netease endpoint.
No request body was captured; the body is described from source only.
What actually happens
You start music recognition while the Netease backend is part of the enabled recognition order.
The popup records from the current tab by default, and the microphone flow records from the microphone after permission is granted.
The extension converts the captured audio into PCM bytes and sends them to a Deno Deploy proxy.
The proxy URL is ncm-recognizer-proxy-0vx43g2d4hq2.foxrefire.deno.net/api, not an official Netease API host.
Data present in the Netease recognition request path
- Audio sampleMono Float32 PCM buffer; a 7.2-second sample at 48 kHz is about 1,382,400 bytes
The sound clip you asked the extension to identify. Can reveal the music, stream, or mic audio present at that time.
- Recognition backendnetease
This determines which outside service receives the audio sample for matching.
- Proxy destinationncm-recognizer-proxy-0vx43g2d4hq2.foxrefire.deno.net
This is where the extension sends the Netease recognition sample before a song result is returned.
Captured request
The shipped code parses the response as JSON and reads response[0].song fields for title, artist, album, and cover art.
The code that does this
The popup records audio and passes each enabled backend into Recognize
Popup recognition loop
popup/guess.jsasync function startTabRecognition() { // Initialize UI for recognition circler.style.opacity = "0" circler.style.display = "flex" circler.style.transition = "opacity 0.3s ease" setTimeout(() => { circler.style.opacity = "1" }, 50) resultTable.style.display = "none" streamProvidersContainer.style.display = "none" notification.classList.remove("show", "pulse", "recognizing") let fallbackRules = await getStorage("fallbackRules") let times = Object.keys(fallbackRules).map(t => Number(t)) let backendsMap = Object.values(fallbackRules) const captureMethod = await getStorage("captureMethod") let tabCaptureAudios = null if (captureMethod === "tabCapture" && typeof chrome !== 'undefined' && chrome.tabCapture) { try { tabCaptureAudios = await recordFromTabCapture(times) } catch(e) { console.error("Tab capture failed:", e) // If error is about tab not playing audio, show noAudioElementsDetected if (e.message && e.message.includes("not playing audio")) { showError(t("noAudioElementsDetected")) } else { showError(t("songNotRecognized")) } return } } else { await recordAudiosInTab(times) } for(let backends of backendsMap) { showStatus(t("listening")) let audios if (tabCaptureAudios) { try { let audio = await tabCaptureAudios.shift() audios = audio ? [audio] : [] } catch(e) { audios = [] } } else { audios = await getNextRecorded().then(r => r.filter(a=> a.length)) } if(!audios.length) { showError(t("noAudioElementsDetected")) return } for(let backend of backends) { let isFound = await getResult(audios, backend) if(isFound) { return } } } showError(t("songNotRecognized"))}async function getResult(audios, backend) { for(let audio of audios) { try{ showStatus(t("queryingWith", [backend])) let result = await Recognize(audio, backend) await writeResult(result) await saveHistory(result) await writeHistory() // Update history display immediately banner.classList.remove("blur") notification.style.display = "none" circler.style.opacity = "0" circler.style.transition = "opacity 0.3s ease" return true } catch(e) { console.log(e) } } return false}Microphone recognition loop
popup/guess.jsasync function startMicRecognition() { try { // Reset UI with smooth transitions resultTable.style.opacity = "0" streamProviders.style.opacity = "0" setTimeout(() => { resultTable.style.display = "none" streamProvidersContainer.style.display = "none" }, 300) circler.style.opacity = "0" circler.style.transition = "opacity 0.3s ease" setTimeout(() => { circler.style.opacity = "1" }, 50) notification.classList.remove("show", "pulse", "recognizing") notification.innerText = "" // Get fallback rules let fallbackRules = await getStorage("fallbackRules") let times = Object.keys(fallbackRules).map(t => Number(t)) let backendsMap = Object.values(fallbackRules) // Record from microphone let micAudios = await recordFromMicrophone(times) // Try recognition with fallback for(let backends of backendsMap) { showStatus(t("listening")) let audio = await micAudios.shift() if(!audio) { showError(t("noAudioRecordedFromMicrophone")) return } for(let backend of backends) { let isFound = await getResult([audio], backend) if(isFound) { return } } } showError(t("songNotRecognizedFromMicrophone")) } catch(e) { console.error("Microphone recognition error:", e) showError(t("failedToAccessMicrophone")) }}The code that does this
The Netease backend converts the sample and posts it to the proxy
Backend selector
backendModules/Recognize.jsexport async function Recognize(audio, backend) { console.log(audio) let backendCall = null switch(backend) { case "shazam": backendCall = shazamGuess break; case "audd": backendCall = auddGuess break case "acr": backendCall = acrGuess break case "tencent": backendCall = tencentGuess break case "netease": backendCall = neteaseGuess break } return await backendCall(audio).then(result => addStreamLinks(result))}Netease request
backendModules/neteaseGuess.jsexport async function neteaseGuess(audio) { let pcm = await convertToPCM(audio) let response = await getResponse(pcm) console.log(JSON.stringify(response)) return { title: response[0].song.name, artist: response[0].song?.artists?.[0]?.name, album: response[0].song.album?.name, art: response[0].song.album?.picUrl }}async function convertToPCM(audio) { // Create AudioContext const audioContext = new AudioContext(); // Convert to ArrayBuffer const arrayBuffer = (audio instanceof ArrayBuffer) ? audio : new Uint8Array(audio).buffer; // Decode it const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); // Original sampleRates const inputChannels = audioBuffer.numberOfChannels; const inputLength = audioBuffer.length; // ---- Monauralize it ---- const monoData = new Float32Array(inputLength); for (let i = 0; i < inputLength; i++) { let sum = 0; for (let ch = 0; ch < inputChannels; ch++) { sum += audioBuffer.getChannelData(ch)[i]; } monoData[i] = sum / inputChannels; } return monoData.buffer}async function getResponse(pcm) { let response = await fetch(`https://ncm-recognizer-proxy-0vx43g2d4hq2.foxrefire.deno.net/api`, { method: "POST", body: pcm }).then(r => r.json()) return response}External host in this Netease recognition path
- ncm-recognizer-proxy-0vx43g2d4hq2.foxrefire.deno.net
Deno Deploy proxy that receives the PCM audio sample for the Netease recognition backend.
What it can do
Permissions this extension asks for, as declared in version 5.9. 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>
See every page you navigate to, as you navigate to it
webNavigation
Store data in your browser
storage
Show you desktop notifications
notifications
Store an unlimited amount of data in your browser
unlimitedStorage
Where it sends data
Destinations our analysis observed Librezam 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.
- ncm-recognizer-proxy-0vx43g2d4hq2.foxrefire.deno.net
Librezam sends data to ncm-recognizer-proxy-0vx43g2d4hq2.foxrefire.deno.net. No other extension we have analyzed sends data here.
