* A method that forwards HTTP Get-methods to the internet to avoid CORS-errors. * * Example input request url: /cors?sendheaders=header1:value1,header2:value2&expectedheaders=header1,header2&url=http://www.test.com/path?param1=value1 * * Only the url-param of the input request url is required. I
(req, res)
| 55 | * @returns {Promise<void>} A promise that resolves when the response is sent |
| 56 | */ |
| 57 | async function cors (req, res) { |
| 58 | if (global.config.cors === "disabled") { |
| 59 | Log.error("CORS is disabled, you need to enable it in `config.js` by setting `cors` to `allowAll` or `allowWhitelist`"); |
| 60 | return res.status(403).json({ error: "CORS proxy is disabled" }); |
| 61 | } |
| 62 | let url; |
| 63 | try { |
| 64 | const urlRegEx = "url=(.+?)$"; |
| 65 | |
| 66 | const match = new RegExp(urlRegEx, "g").exec(req.url); |
| 67 | if (!match) { |
| 68 | url = `invalid url: ${req.url}`; |
| 69 | Log.error(url); |
| 70 | return res.status(400).send(url); |
| 71 | } else { |
| 72 | url = match[1]; |
| 73 | if (typeof global.config !== "undefined") { |
| 74 | if (config.hideConfigSecrets) { |
| 75 | url = replaceSecretPlaceholder(url); |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | // Validate protocol before attempting connection (non-http/https are never allowed) |
| 80 | let parsed; |
| 81 | try { |
| 82 | parsed = new URL(url); |
| 83 | } catch { |
| 84 | Log.warn(`SSRF blocked (invalid URL): ${url}`); |
| 85 | return res.status(403).json({ error: "Forbidden: private or reserved addresses are not allowed" }); |
| 86 | } |
| 87 | if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { |
| 88 | Log.warn(`SSRF blocked (protocol): ${url}`); |
| 89 | return res.status(403).json({ error: "Forbidden: private or reserved addresses are not allowed" }); |
| 90 | } |
| 91 | |
| 92 | // Block localhost by hostname before even creating the dispatcher (no DNS needed). |
| 93 | if (parsed.hostname.toLowerCase() === "localhost") { |
| 94 | Log.warn(`SSRF blocked (localhost): ${url}`); |
| 95 | return res.status(403).json({ error: "Forbidden: private or reserved addresses are not allowed" }); |
| 96 | } |
| 97 | |
| 98 | // Whitelist check: if enabled, only allow explicitly listed domains |
| 99 | if (global.config.cors === "allowWhitelist" && !global.config.corsDomainWhitelist.includes(parsed.hostname.toLowerCase())) { |
| 100 | Log.warn(`CORS blocked (not in whitelist): ${url}`); |
| 101 | return res.status(403).json({ error: "Forbidden: domain not in corsDomainWhitelist" }); |
| 102 | } |
| 103 | |
| 104 | const headersToSend = getHeadersToSend(req.url); |
| 105 | const expectedReceivedHeaders = geExpectedReceivedHeaders(req.url); |
| 106 | Log.log(`cors url: ${url}`); |
| 107 | |
| 108 | // Resolve DNS once and validate the IP. The validated IP is then pinned |
| 109 | // for the actual connection so fetch() cannot re-resolve to a different |
| 110 | // address. This prevents DNS rebinding / TOCTOU attacks (GHSA-xhvw-r95j-xm4v). |
| 111 | const { address, family } = await dns.promises.lookup(parsed.hostname); |
| 112 | if (ipaddr.process(address).range() !== "unicast") { |
| 113 | Log.warn(`SSRF blocked: ${url}`); |
| 114 | return res.status(403).json({ error: "Forbidden: private or reserved addresses are not allowed" }); |
no test coverage detected