{"record":{"id":"2e120b8e9aa383a3","repo":"MagicMirrorOrg/MagicMirror","slug":"forbidden-domain-not-in-corsdomainwhitelist","errorCode":null,"errorMessage":"Forbidden: domain not in corsDomainWhitelist","messagePattern":"Forbidden: domain not in corsDomainWhitelist","errorType":"http","errorClass":null,"httpStatus":403,"severity":"warning","filePath":"js/server_functions.js","lineNumber":101,"sourceCode":"\t\t\t} catch {\n\t\t\t\tLog.warn(`SSRF blocked (invalid URL): ${url}`);\n\t\t\t\treturn res.status(403).json({ error: \"Forbidden: private or reserved addresses are not allowed\" });\n\t\t\t}\n\t\t\tif (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n\t\t\t\tLog.warn(`SSRF blocked (protocol): ${url}`);\n\t\t\t\treturn res.status(403).json({ error: \"Forbidden: private or reserved addresses are not allowed\" });\n\t\t\t}\n\n\t\t\t// Block localhost by hostname before even creating the dispatcher (no DNS needed).\n\t\t\tif (parsed.hostname.toLowerCase() === \"localhost\") {\n\t\t\t\tLog.warn(`SSRF blocked (localhost): ${url}`);\n\t\t\t\treturn res.status(403).json({ error: \"Forbidden: private or reserved addresses are not allowed\" });\n\t\t\t}\n\n\t\t\t// Whitelist check: if enabled, only allow explicitly listed domains\n\t\t\tif (global.config.cors === \"allowWhitelist\" && !global.config.corsDomainWhitelist.includes(parsed.hostname.toLowerCase())) {\n\t\t\t\tLog.warn(`CORS blocked (not in whitelist): ${url}`);\n\t\t\t\treturn res.status(403).json({ error: \"Forbidden: domain not in corsDomainWhitelist\" });\n\t\t\t}\n\n\t\t\tconst headersToSend = getHeadersToSend(req.url);\n\t\t\tconst expectedReceivedHeaders = geExpectedReceivedHeaders(req.url);\n\t\t\tLog.log(`cors url: ${url}`);\n\n\t\t\t// Resolve DNS once and validate the IP. The validated IP is then pinned\n\t\t\t// for the actual connection so fetch() cannot re-resolve to a different\n\t\t\t// address. This prevents DNS rebinding / TOCTOU attacks (GHSA-xhvw-r95j-xm4v).\n\t\t\tconst { address, family } = await dns.promises.lookup(parsed.hostname);\n\t\t\tif (ipaddr.process(address).range() !== \"unicast\") {\n\t\t\t\tLog.warn(`SSRF blocked: ${url}`);\n\t\t\t\treturn res.status(403).json({ error: \"Forbidden: private or reserved addresses are not allowed\" });\n\t\t\t}\n\n\t\t\t// Pin the validated IP — fetch() reuses it instead of doing its own DNS lookup\n\t\t\tconst dispatcher = new undici.Agent({\n\t\t\t\tconnect: {","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/MagicMirrorOrg/MagicMirror/blob/4b4a59534f7da01e4030e46029fe9dd649a7675e/js/server_functions.js#L83-L119","documentation":"When MagicMirror's `cors` config is set to \"allowWhitelist\", the /cors proxy only forwards requests whose target hostname appears in the `corsDomainWhitelist` array (compared lowercase). Any other domain is refused with 403 'domain not in corsDomainWhitelist'. This is an intentional configuration gate, not a network failure.","triggerScenarios":"config.js contains `cors: \"allowWhitelist\"` and a module requests GET /cors?url=https://some-domain.com/... where some-domain.com (lowercased hostname) is not an entry of `corsDomainWhitelist`.","commonSituations":"Switching from default/allowAll to allowWhitelist without updating the whitelist; adding 'https://example.com/path' (full URL) or 'WWW.Example.com' (case) to the whitelist instead of the bare lowercase hostname 'example.com'; a feed changing domains (e.g. redirect to a CDN).","solutions":["Add the exact bare hostname to corsDomainWhitelist in config.js, e.g. corsDomainWhitelist: [\"example.com\"]","Whitelist entries are hostname-only and matched after toLowerCase — do not include scheme, path, or uppercase letters","Alternatively set cors: \"allowAll\" in config.js if you accept the security tradeoff","If the target legitimately redirects to another host, whitelist the redirected hostname too"],"exampleFix":"// before (config.js)\ncors: \"allowWhitelist\",\ncorsDomainWhitelist: [\"https://calendar.example.com/feed.ics\"]\n// after\ncors: \"allowWhitelist\",\ncorsDomainWhitelist: [\"calendar.example.com\"]","handlingStrategy":"validation","validationCode":"const host = new URL(feedUrl).hostname.toLowerCase();\nif (window.MM_CONFIG_whitelist && !window.MM_CONFIG_whitelist.includes(host)) {\n  console.warn(`${host} missing from corsDomainWhitelist in config.js`);\n}","typeGuard":"function isWhitelisted(u, whitelist) {\n  try { return whitelist.map(h => h.toLowerCase()).includes(new URL(u).hostname.toLowerCase()); } catch { return false; }\n}","tryCatchPattern":"const res = await fetch(proxyUrl);\nif (res.status === 403 && (await res.json()).error.includes(\"corsDomainWhitelist\")) {\n  throw new Error(`Add ${new URL(feedUrl).hostname} to corsDomainWhitelist in config.js`);\n}","preventionTips":["Store whitelist entries as bare lowercase hostnames only (no scheme/path)","After enabling allowWhitelist, test every module's feed once","Whitelist all hosts a feed may redirect to","Keep whitelist in one place in config.js with comments"],"tags":["whitelist","configuration","http-403","cors-proxy"],"backgroundTag":"cors-domain-whitelist-rejected","analyzedSha":"4b4a59534f7da01e4030e46029fe9dd649a7675e","analyzedAt":"2026-08-31T21:49:42.591Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}