var dns = require('dns').promises; var net = require('net'); function isPrivateIp(ip) { if (!ip) return true; if (net.isIPv4(ip)) { var p = ip.split('.').map(Number); if (p[0] === 10 || p[0] === 127 || p[0] === 0) return true; if (p[0] === 100 && p[1] >= 64 && p[1] <= 127) return true; if (p[0] === 169 && p[1] === 254) return true; if (p[0] === 172 && p[1] >= 16 && p[1] <= 31) return true; if (p[0] === 192 && (p[1] === 168 || p[1] === 0 || (p[1] === 88 && p[2] === 99))) return true; if (p[0] === 198 && (p[1] === 18 || p[1] === 19 || (p[1] === 51 && p[2] === 100))) return true; if (p[0] === 203 && p[1] === 0 && p[2] === 113) return true; if (p[0] >= 224) return true; return false; } if (net.isIPv6(ip)) { if (ip.includes('%')) return true; var low = new URL('http://[' + ip + ']').hostname.slice(1, -1).toLowerCase(); // Global unicast only: excludes mapped/NAT64/local/multicast addresses. if (!/^[23][0-9a-f]{3}:/.test(low)) return true; var parts = low.split(':'); var second = parseInt(parts[1] || '0', 16); // IETF special 2001::/23, documentation 2001:db8::/32 and 3fff::/20, // plus 6to4 and retired 6bone. Ordinary public 2001:: addresses remain usable. return (parts[0] === '2001' && (second < 0x200 || second === 0xdb8)) || parts[0] === '2002' || parts[0] === '3ffe' || (parts[0] === '3fff' && second < 0x1000); } return true; } async function assertSafeHttpsUrl(urlStr, label) { var u; try { u = new URL(urlStr); } catch (e) { throw new Error((label || 'URL') + ' is invalid'); } if (u.protocol !== 'https:') throw new Error((label || 'URL') + ' must use https://'); if (u.username || u.password) throw new Error((label || 'URL') + ' must not include credentials'); var addrs = await dns.lookup(u.hostname.replace(/^\[|\]$/g, ''), { all: true }); if (!addrs.length) throw new Error((label || 'URL') + ' did not resolve'); for (var i = 0; i < addrs.length; i++) { if (isPrivateIp(addrs[i].address)) throw new Error((label || 'URL') + ' resolves to a private IP'); } return u; } module.exports = { assertSafeHttpsUrl: assertSafeHttpsUrl, isPrivateIp: isPrivateIp };