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
45Risk

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

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
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.

02EvidenceFIELD TABLE
Data present in the Netease recognition request path
FieldValueWhy it matters
Audio sample
Mono Float32 PCM buffer; a 7.2-second sample at 48 kHz is about 1,382,400 bytesThe sound clip you asked the extension to identify. Can reveal the music, stream, or mic audio present at that time.
Recognition backend
neteaseThis determines which outside service receives the audio sample for matching.
Proxy destination
ncm-recognizer-proxy-0vx43g2d4hq2.foxrefire.deno.netThis is where the extension sends the Netease recognition sample before a song result is returned.
03EvidenceNETWORK CAPTURE
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.
04EvidenceCODE COMPARE
The code that does this

The popup records audio and passes each enabled backend into Recognize

What it actually does
Popup recognition looppopup/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 looppopup/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"))
    }
}
05EvidenceCODE COMPARE
The code that does this

The Netease backend converts the sample and posts it to the proxy

What it actually does
Backend selectorbackendModules/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 requestbackendModules/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
}
06EvidenceTHIRD PARTY LIST
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.

Data recipients

ncm-recognizer-proxy-0vx43g2d4hq2.foxrefire.deno.net
Updated 17 September 2026amo-2865573