Résumé
MagicMirror socket payload secret placeholder expansion can disclose SECRET_* environment variables
Détails de l’avis
Summary
When hideConfigSecrets: true is enabled, MagicMirror redacts SECRET_* environment placeholders in the HTTP /config response, but the shared node-helper socket dispatcher expands **SECRET_NAME** placeholders in every inbound socket payload before passing it to module helpers. Any client that can connect to a loaded module namespace can send a placeholder such as **SECRET_API_KEY** and cause the server to substitute the real environment variable into the helper payload. Helpers that echo attacker-controlled payload fields, such as the default weather helper error path, can return the secret value to the socket client.
Details
The affected product is the npm package/application magicmirror at version 2.36.0, tested at commit fb41d24ef522e91e802e2a623ff6afbddeb3c9d8 from https://github.com/MagicMirrorOrg/MagicMirror.git.
The secret-redaction feature is implemented during config loading:
js/utils.js:117-123loads aconfig.envfile next to the config file intoprocess.envwhen present.js/utils.js:130-151creates both a full config and a redacted config.js/utils.js:137-140redacts environment variables whose names start withSECRET_to**SECRET_NAME**in the redacted config whenhideConfigSecrets: trueis present.js/server.js:112-125returns eitherconfigObj.redactedConforconfigObj.fullConffrom/configdepending onconfig.hideConfigSecrets.
The disclosure root cause is the inbound socket dispatcher:
js/node_helper.js:88-103registers a catch-all handler for each module namespace.js/node_helper.js:91-99checksconfig?.hideConfigSecretsand, for every inbound object payload, runsreplaceSecretPlaceholder(JSON.stringify(payload))before invokingsocketNotificationReceived(...).js/server_functions.js:23-34implementsreplaceSecretPlaceholder(...)by replacing**SECRET_* **-style placeholders withprocess.env[...], unlessglobal.config.cors === "allowAll".
This reverses the redaction boundary: redacted placeholders intended for the browser can be sent back to the server and expanded into real environment secret values inside helper payloads.
A confirmed echo path exists in the default weather helper:
defaultmodules/weather/node_helper.js:12-19acceptsINIT_WEATHERfrom the socket.defaultmodules/weather/node_helper.js:27-31copiesconfig.instanceIdfrom the attacker-controlled payload.defaultmodules/weather/node_helper.js:47-52attempts to dynamically load the requested weather provider.defaultmodules/weather/node_helper.js:86-91catches errors and sendsWEATHER_ERRORwith the sameinstanceIdback to the namespace.
False-positive screening performed:
- This is not a generic environment leak through
/env;js/server_functions.js:221-240returns only selected client environment paths. - The HTTP
/configroute does redact placeholders whenhideConfigSecrets: true; the issue is that the inbound socket path expands those placeholders again before module helper code runs. replaceSecretPlaceholder(...)intentionally refuses substitution whenglobal.config.cors === "allowAll"(js/server_functions.js:29-34); the positive PoC usedcors: "disabled", which is the shipped default (js/defaults.js:14). A negative control with no substitution produced the placeholder unchanged.- The attacker must know or infer a
SECRET_*variable name. If the attacker can read the redacted/configresponse, placeholder names may be disclosed even when values are hidden. The PoC uses a knownSECRET_MM_AUDITtest variable. - Network reachability follows the Socket.IO exposure model. With the shipped default
address: "localhost"and loopbackipWhitelist, remote network reachability is limited. In documented non-loopback deployments, this combines with the Socket.IO access-control gap described separately.
Affected-version evidence: only magicmirror@2.36.0 at commit fb41d24ef522e91e802e2a623ff6afbddeb3c9d8 was tested. The affected range is unknown from this audit; earlier versions were not tested. No patched version or fix commit was identified locally.
PoC
The following safe local PoC was run from a clean checkout of MagicMirror at commit fb41d24ef522e91e802e2a623ff6afbddeb3c9d8. Because node_modules were not installed in this audit environment and package.json:52 has a destructive postinstall, the command uses dependency stubs while executing the vulnerable repository dispatcher and weather helper code. It writes no files and does not contact external services.
Positive trigger:
node -e 'const Module=require("module"); const orig=Module._load; Module._load=(r,p,m)=>{ if(r==="express") return { static:()=>()=>{} }; if(r==="logger") return {log(){},error(){},warn(){},info(){},debug(){}}; if(r==="#server_functions") return {replaceSecretPlaceholder:(input)=>input.replaceAll(/\*\*(SECRET_[^*]+)\*\*/g,(_m,g)=>process.env[g])}; return orig(r,p,m); }; require("./js/alias-resolver"); global.root_path=process.cwd(); global.config={hideConfigSecrets:true,cors:"disabled"}; process.env.SECRET_MM_AUDIT="secret-marker-42"; const Weather=require("./defaultmodules/weather/node_helper"); const helper=new Weather(); helper.setName("weather"); const sent=[]; helper.sendSocketNotification=(n,p)=>sent.push({n,p}); let onAny; const fakeIo={of(){return {on(_ev,cb){const socket={onAny(fn){onAny=fn;}}; cb(socket);}};}}; helper.setSocketIO(fakeIo); Promise.resolve(onAny("INIT_WEATHER",{instanceId:"**SECRET_MM_AUDIT**",weatherProvider:"definitely-not-a-provider",type:"current"})).then(()=>setTimeout(()=>{console.log(JSON.stringify(sent));},10));'
Observed output:
[{"n":"WEATHER_ERROR","p":{"instanceId":"secret-marker-42","error":"Cannot find module '/home/sondt23/Github/Research/CVE/auto-github-cve/github-repo/MagicMirror/defaultmodules/weather/providers/definitely-not-a-provider.js'\nRequire stack:\n- /home/sondt23/Github/Research/CVE/auto-github-cve/github-repo/MagicMirror/defaultmodules/weather/node_helper.js\n- /home/sondt23/Github/Research/CVE/auto-github-cve/github-repo/MagicMirror/[eval]"}}]
Expected vulnerable output: the weather error payload returned by the helper contains "instanceId":"secret-marker-42", proving the server substituted the SECRET_MM_AUDIT environment variable into an attacker-controlled socket payload and returned it to the client.
Negative/control trigger simulating no inbound placeholder substitution:
node -e 'const Module=require("module"); const orig=Module._load; Module._load=(r,p,m)=>{ if(r==="express") return { static:()=>()=>{} }; if(r==="logger") return {log(){},error(){},warn(){},info(){},debug(){}}; if(r==="#server_functions") return {replaceSecretPlaceholder:(input)=>input}; return orig(r,p,m); }; require("./js/alias-resolver"); global.root_path=process.cwd(); global.config={hideConfigSecrets:true,cors:"allowAll"}; process.env.SECRET_MM_AUDIT="secret-marker-42"; const Weather=require("./defaultmodules/weather/node_helper"); const helper=new Weather(); helper.setName("weather"); const sent=[]; helper.sendSocketNotification=(n,p)=>sent.push({n,p}); let onAny; const fakeIo={of(){return {on(_ev,cb){const socket={onAny(fn){onAny=fn;}}; cb(socket);}};}}; helper.setSocketIO(fakeIo); Promise.resolve(onAny("INIT_WEATHER",{instanceId:"**SECRET_MM_AUDIT**",weatherProvider:"definitely-not-a-provider",type:"current"})).then(()=>setTimeout(()=>{console.log(JSON.stringify(sent));},10));'
Observed control output:
[{"n":"WEATHER_ERROR","p":{"instanceId":"**SECRET_MM_AUDIT**","error":"Cannot find module '/home/sondt23/Github/Research/CVE/auto-github-cve/github-repo/MagicMirror/defaultmodules/weather/providers/definitely-not-a-provider.js'\nRequire stack:\n- /home/sondt23/Github/Research/CVE/auto-github-cve/github-repo/MagicMirror/defaultmodules/weather/node_helper.js\n- /home/sondt23/Github/Research/CVE/auto-github-cve/github-repo/MagicMirror/[eval]"}}]
Exp
Références
- https://github.com/advisories/GHSA-q4gh-4ffp-5cg8
- https://github.com/MagicMirrorOrg/MagicMirror/security/advisories/GHSA-q4gh-4ffp-5cg8
- https://github.com/MagicMirrorOrg/MagicMirror/pull/4184
- https://github.com/MagicMirrorOrg/MagicMirror/commit/ca7b752025962441196e148f8c1bc04b90117979
- https://github.com/MagicMirrorOrg/MagicMirror/releases/tag/v2.37.0
Vulnérabilités liées
Tout Supply chain →- HIGHCVE-2026-75912
CodeWhale: Argument Injection in `git_blame` Tool Allows Arbitrary File Read Without Approval
- HIGHCVE-2026-75915
CodeWhale: js_execution leaks parent environment to model context via missing env scrub
- HIGHCVE-2026-75859
CodeWhale: Project config `instructions` override enables arbitrary file read into AI system prompt via cloned repository
- HIGHCVE-2026-72804
SiYuan: Graph endpoints omit the publish-password tier: anonymous readers receive block-level content of password-protected documents
- MEDIUMCVE-2026-61842
Grav: Twig sandbox config exfiltration via grav.offsetGet + dump filter (CVE-2026-44738 bypass)
- MEDIUMCVE-2026-73229
Django REST framework: AdminRenderer may disclose GET-protected data when rendering invalid write requests