Is Librezam safe?

Medium risk

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.

FoxRefirev5.9Firefox Add-ons
47Risk

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

MediumunwantedCWE-200Dynamic sandbox

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.

What actually happens

You did this

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 did this

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 sample
    Mono 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 backend
    netease

    This determines which outside service receives the audio sample for matching.

  • Proxy destination
    ncm-recognizer-proxy-0vx43g2d4hq2.foxrefire.deno.net

    This is where the extension sends the Netease recognition sample before a song result is returned.

Captured request

POSThttps://ncm-recognizer-proxy-0vx43g2d4hq2.foxrefire.deno.net/api

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

Readable version

Popup recognition loop

popup/guess.js
async 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.js
async 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

Readable version

Backend selector

backendModules/Recognize.js
export 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.js
export 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.

Updated 2 November 2026amo-2865573