CVE-2026-63641: MagicMirror Socket.IO module namespaces bypass configured IP whitelist and allow unauthenticated server-side actions
Summary MagicMirror applies ipWhitelist only as Express middleware, but the Socket.IO server is attached directly to the HTTP server without equivalent IP allowlist, origin, or namespace authentication checks. In a documented common deployment where MagicMirror listens on a non-loopback interface but expects ipWhitelist to restrict access, an untrusted network client can connect directly to module Socket.IO namespaces and send arbitrary module-helper notifications. This allows unauthenticated server-side requests through default modules and can reach command execution in the default updatenotification helper when a third-party module update is pending and the attacker supplies the update command through the trusted socket configuration path.
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.
Default committed settings bind to loopback and allow loopback only (js/defaults.js:8-13), so the remote network impact requires a documented common configuration where the server is reachable beyond loopback. The shipped sample explicitly documents non-loopback binding and IP allowlist behavior: config/config.js.sample:11-20 says address may be another interface or 0.0.0.0/::, and ipWhitelist controls allowed clients.
The trust-boundary issue is that Socket.IO is configured before and outside the Express middleware chain:
- js/server.js:42-50 creates Socket.IO directly on the HTTP(S) server with cors.origin: /.$/. - js/server.js:89-90 applies ipAccessControl(config.ipWhitelist) only with app.use(...), which protects Express routes and static files but not Socket.IO handshakes or namespaces. - A search of runtime files found no allowRequest, io.use(...), handshake IP check, or namespace authentication for Socket.IO; the only relevant matches were js/server.js:44 and js/server.js:90. - js/nodehelper.js:88-103 registers every module namespace and dispatches every socket event and payload directly to socketNotificationReceived(...).
Once a client can reach the Socket.IO server, the following default-module server-side actions are reachable without an equivalent IP whitelist or module-authentication check:
- defaultmodules/newsfeed/nodehelper.js:12-17 accepts CHECKARTICLEURL and calls checkArticleUrl(payload.url). - defaultmodules/newsfeed/nodehelper.js:25-38 performs fetch(url, { method: "HEAD" }) on the supplied URL and sends the result back. - defaultmodules/calendar/nodehelper.js:13-24 accepts ADDCALENDAR/FETCHCALENDAR socket messages. - defaultmodules/calendar/nodehelper.js:40-58 accepts an arbitrary syntactically valid calendar URL and creates a fetcher. - defaultmodules/calendar/calendarfetcher.js:35-44 passes the URL into HTTPFetcher, whose fetch sink is js/httpfetcher.js:286-294. - defaultmodules/updatenotification/nodehelper.js:46-68 accepts CONFIG, MODULES, and SCANUPDATES notifications and trusts the socket-provided config/module list. - defaultmodules/updatenotification/updatehelper.js:43-47 stores update commands from config. - defaultmodules/updatenotification/updatehelper.js:96-116 executes the selected update command with childprocess.exec in the module directory. - defaultmodules/updatenotification/updatehelper.js:221-227 looks up the command from config.updates by module name.
False-positive screening performed:
- Express HTTP routes are protected by ipAccessControl(config.ipWhitelist) at js/server.js:89-90; this does not protect Socket.IO because Socket.IO is attached to the raw HTTP server and no Socket.IO middleware was found. - The explicit /cors HTTP endpoint has separate SSRF mitigations (js/serverfunctions.js:47-117) and is disabled by default (js/defaults.js:14); the confirmed request primitive here uses module-helper socket paths, not /cors. - The command-execution variant is not an unconditional default RCE: updatenotification only executes an update command for a non-core git-managed module that is considered behind. However, the trusted command source is attacker-controlled through the unauthenticated socket CONFIG message once this boundary is crossed. - Default loopback-only binding lowers default remote exposure, but the sample configuration documents exactly the deployment model where users rely on ipWhitelist for network restrictions.
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 PoCs were run from a clean checkout of MagicMirror at commit fb41d24ef522e91e802e2a623ff6afbddeb3c9d8. Because nodemodules were not installed in this audit environment and package.json:52 has a destructive postinstall (git clean -df fonts vendor modules/default), the commands use small Node harnesses with stubs for missing dependencies while exercising the vulnerable repository code paths directly. They do not contact external hosts and write only disposable /tmp marker files.
1. Confirm that the runtime lacks Socket.IO IP allowlist controls:
bash grep -RIn --exclude-dir=nodemodules --exclude-dir=.git --exclude-dir=.claude --exclude-dir=reports -E "allowRequest|io\.use\(|handshake|ipAccessControl\(|cors: \{|origin: /\.\\$/" js defaultmodules serveronly config tests
Observed output:
text js/server.js:44: cors: { js/server.js:90: app.use(ipAccessControl(config.ipWhitelist));
This confirms the IP allowlist appears only as Express middleware and no Socket.IO handshake/namespace allowlist was present in the reviewed runtime files.
2. Confirm a default module helper will perform a server-side request to an attacker-supplied loopback URL when driven through its socket notification handler:
bash node -e 'const Module=require("module"); const orig=Module.load; Module.load=(r,p,m)=>{ if(r==="logger") return {log(){},error(){},warn(){},info(){},debug(){}}; if(r==="nodehelper") return {create:(o)=>function(){Object.assign(this,o);this.sendSocketNotification=(n,p)=>console.log("SOCKET",n,JSON.stringify(p));}}; if(r==="./newsfeedfetcher") return function(){}; return orig(r,p,m); }; const calls=[]; global.fetch=async(url,opts)=>{calls.push({url,opts}); return {headers:{get:(h)=>h==="x-frame-options"?"deny":null}};}; const Helper=require("./defaultmodules/newsfeed/nodehelper"); const h=new Helper(); h.start(); h.socketNotificationReceived("CHECKARTICLEURL",{url:"http://127.0.0.1:65535/internal"}); setTimeout(()=>console.log("fetchCalls=",JSON.stringify(calls)),10);'
Observed output:
text SOCKET ARTICLEURLSTATUS {"url":"http://127.0.0.1:65535/internal","canFrame":false} fetchCalls= [{"url":"http://127.0.0.1:65535/internal","opts":{"method":"HEAD"}}]
Expected vulnerable output: the harness records a server-side HEAD request to http://127.0.0.1:65535/internal even though /cors SSRF protections are not involved.
3. Confirm the conditional command-execution sink is reachable from the trusted socket-driven update path using a harmless /tmp marker command:
bash rm -f /tmp/mm-rce-marker mkdir -p /tmp/mm-audit/modules/evil /tmp/mm-audit/defaultmodules node -e 'const fs=require("node:fs"); const Module=require("module"); const orig=Module.load; Module.load=(r,p,m)=>{ if(r==="logger") return {log(){},error(){},warn(){},info(){},debug(){}}; if(r==="nodehelper") return {create:(o)=>function(){Object.assign(this,o);this.sendSocketNotification=()=>{};}}; return orig(r,p,m); }; global.rootpath="/tmp/mm-audit"; global.defaultModulesDir="defaultmodules"; fs.writeFileSync("/tmp/mm-audit/defaultmodules/defaultmodules.js","module.exports=[]"); const Helper=require("./defaultmodules/updatenotification/nodehelper"); const h=new Helper(); h.gitHelper={add:async()=>{},getRepos:async()=>[{module:"evil",behind:1}],checkUpdates:()=>[{module:"evil",behind:1}]}; h.sendSocketNotification=(n,p)=>console.log("SOCKET",n,JSON.stringify(p)); (async()=>{ await h.socketNotificationReceived("CONFIG",{updates:[{evil:"printf ok > /tmp/mm-rce-marker"}],updateTimeout:5000,updateAutorestart:false,ignoreModules:[],sendUpdatesNotifications:false,updateInterval:60000,useModulesFromConfig:true}); await h.socketNotificationReceived("MODULES",["evil"]); console.log("marker=",fs.readFileSync("/tmp/mm-rce-marker","utf8")); process.exit(0); })();' rm -f /tmp/mm-rce-marker rm -rf /tmp/mm-audit
Observed output from the executed harness:
text SOCKET REPOSTATUS {"module":"evil","behind":1} SOCKET UPDATESTATUS {"name":"evil","updateCommand":"printf ok > /tmp/mm-rce-marker","inProgress":true,"error":false,"updated":true,"needRestart":true} marker= ok
Expected vulnerable output: marker= ok demonstrates the update command supplied through the trusted socket configuration path reached childprocess.exec and wrote the harmless marker.
Negative/control cases:
- With the shipped committed defaults (js/defaults.js:8-13), the server binds localhost and ipWhitelist includes only loopback, so a remote network attacker cannot reach either HTTP or Socket.IO unless the deployment is changed to a documented non-loopback address. - The updatenotification command execution path requires at least one non-core module update result; if checkUpdates() returns no third-party module with behind > 0, updatehelper.parse(...) does not execute an update command. - The explicit /cors route was not used for the confirmed request primitive and has separate protocol/hostname/DNS checks.
Final repro re-check: the sink search, newsfeed socket request harness, and update marker harness were re-run after drafting; the observed outputs above are from this environment. Cleanup removed /tmp/mm-rce-marker and /tmp/mm-audit.
Impact In a documented common non-loopback deployment that relies on ipWhitelist for access control, an unauthenticated network client can bypass the intended IP allowlist for Socket.IO module namespaces. The attacker can send arbitrary module-helper notifications and payloads as if they were a trusted browser client.
Confirmed impacts include:
- Confidentiality/SSRF: server-side requests to attacker-chosen URLs through default module helpers, including loopback/internal URLs. The newsfeed PoC shows a HEAD request to 127.0.0.1. - Integrity/availability: arbitrary manipulation of module-helper state and periodic fetch/update behavior through trusted socket messages. - Conditional code execution: when a third-party git-managed module is considered behind by the update checker, an attacker can supply a command in the socket CONFIG payload and trigger childprocess.exec; the PoC safely wrote a /tmp marker.
CVSS 3.1 rationale for the primary trust-boundary bypass with confirmed SSRF and conditional RCE variant: AV:A because MagicMirror's security policy says it is intended for trusted local/private networks and the documented non-loopback exposure is LAN-style; AC:H because default loopback settings must be changed and the RCE variant requires a pending third-party module update, though SSRF requires fewer conditions after reachability; PR:N because no application authentication is required; UI:N because the attacker connects directly to Socket.IO; S:C because the vulnerable application can cause requests/actions against other local/internal services and can execute commands in a child process in the conditional variant; C:L/I:L/A:L for confirmed internal request and helper-state/command side effects, with conservative scoring because unconditional default RCE was not shown.
Suggested remediation Apply the same access-control decision to Socket.IO handshakes and namespaces as to Express routes. Concretely, add a Socket.IO allowRequest or io.use(...) middleware that normalizes socket.handshake.address/request IPs with the same ipAccessControl logic, and reject clients not allowed by config.ipWhitelist. Avoid relying on CORS for authorization; keep origin checks as a browser hardening layer only.
Also add module-helper authorization so arbitrary clients cannot send privileged server-side module notifications. For example, issue an unguessable per-session/module token to the served client and require it on helper messages, or separate read-only client events from privileged server-maintenance actions.
For defense in depth:
- Add SSRF protections or allowlists to calendar/newsfeed/weather helper fetch paths, not only /cors. - Do not accept updatenotification update commands from socket payloads; use the server-loaded config only, validate module names against configured modules, and avoid shell execution where possible. - Add regression tests proving a disallowed IP receives a rejected Socket.IO handshake even when Express routes are protected, and proving /newsfeed//calendar helper messages from unauthenticated sockets cannot trigger server-side requests or update commands.
Other sources
MagicMirror² is an open source modular smart mirror platform. Prior to 2.37.0, MagicMirror applies ipWhitelist only as Express middleware, while the Socket.IO server in js/server.js is attached directly to the HTTP server without equivalent IP allowlist, origin, or namespace authentication checks. In a documented non-loopback deployment that relies on ipWhitelist, an unauthenticated adjacent-network client can connect directly to module Socket.IO namespaces, and js/nodehelper.js dispatches arbitrary events and payloads to socketNotificationReceived. The default newsfeed and calendar helpers can make server-side requests to attacker-selected URLs, while the default updatenotification helper can reach childprocess.exec when a third-party module update is pending and the attacker supplies an update command through the socket CONFIG path. This can expose internal services, manipulate module-helper state, and conditionally execute commands. This issue is fixed in version 2.37.0.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/magicmirrorto a version that resolves this vulnerability.Fixed in 2.37.0 - Upgrade
Upgrade to a fixed release to a version that resolves this vulnerability.
Fixed in 2.37.0 - Configuration
Add Socket.IO IP allowlisting enforcement by implementing `allowRequest` or `io.use(...)` middleware that normalizes the client IPs (e.g., `socket.handshake.address`/request IPs) and rejects connections not permitted by `config.ipWhitelist`. Ensure this is applied to Socket.IO handshakes and module namespaces in addition to the existing Express `app.use(ipAccessControl(config.ipWhitelist))` at `js/server.js:89-90` (which currently does not protect Socket.IO).
MagicMirror Socket.IO server (js/server.js) ipAccessControl(config.ipWhitelist) = Apply the same IP allowlist logic used for Express middleware to Socket.IO handshakes/namespaces - Configuration
Do not accept `updatenotification` update commands or privileged update configuration (e.g., socket `CONFIG` payloads and socket-provided module lists used for update behavior) from socket message payloads. Use only the server-loaded configuration, validate module names against the configured modules list, and avoid executing any shell-based command coming from socket payloads. Specifically, prevent attacker-controlled socket `CONFIG` from reaching the `child_process.exec` path in `defaultmodules/updatenotification/update_helper.js:96-116`.
defaultmodules/updatenotification (defaultmodules/updatenotification/node_helper.js + update_helper.js) Trusted socket source for CONFIG/MODULES/SCAN_UPDATES = Only accept server-loaded config; disallow socket-supplied privileged update config - Compensating control
For defense in depth, implement an additional privileged authorization mechanism for server-maintenance module notifications (e.g., per-session/per-module unguessable token for privileged helper messages). Separate read-only client events from privileged server-maintenance actions so unauthenticated adjacent-network clients cannot trigger helper-side effects via Socket.IO.
Event History
Frequently Asked Questions
Which deployments are realistically exposed?
Deployments that expose MagicMirror on a non-loopback interface and rely on ipWhitelist are affected before 2.37.0. The whitelist was enforced only for Express requests, not for direct Socket.IO connections to module namespaces.
What does an attacker need to exploit this?
An unauthenticated client on an adjacent network can connect directly to module Socket.IO namespaces. No origin validation or namespace authentication checks were applied on this path.
What server-side impact is possible?
The default newsfeed and calendar helpers can be induced to make server-side requests to attacker-selected URLs. The default updatenotification helper can conditionally reach child_process.exec when a third-party module update is pending and an attacker supplies an update command through the socket CONFIG path.
What should be done if the instance cannot be patched immediately?
Update MagicMirror² to version 2.37.0. If updating is not immediately possible, avoid non-loopback exposure and do not treat ipWhitelist as protection for Socket.IO module connections.