50 lines
2.2 KiB
JavaScript
50 lines
2.2 KiB
JavaScript
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
|
|
};
|