57 lines
1.5 KiB
JavaScript
57 lines
1.5 KiB
JavaScript
/**
|
|
* Public HTTP(S) only — private / loopback / metadata hosts are blocked.
|
|
*/
|
|
|
|
function isBlockedHostname(hostname) {
|
|
if (!hostname) return true;
|
|
const h = String(hostname).toLowerCase().replace(/^\[|\]$/g, '');
|
|
if (
|
|
h === 'localhost' ||
|
|
h.endsWith('.localhost') ||
|
|
h === '0.0.0.0' ||
|
|
h === '::' ||
|
|
h === '::1' ||
|
|
h === 'metadata.google.internal' ||
|
|
h.endsWith('.internal')
|
|
) {
|
|
return true;
|
|
}
|
|
const v4 = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
if (v4) {
|
|
const a = Number(v4[1]);
|
|
const b = Number(v4[2]);
|
|
if (a === 0 || a === 10 || a === 127) return true;
|
|
if (a === 169 && b === 254) return true;
|
|
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
if (a === 192 && b === 168) return true;
|
|
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
}
|
|
if (h.includes(':')) {
|
|
if (h === '::1' || h.startsWith('fe80:') || h.startsWith('fc') || h.startsWith('fd')) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function assertHttpUrl(raw) {
|
|
let url;
|
|
try {
|
|
url = new URL(String(raw));
|
|
} catch (_) {
|
|
throw new Error('invalid URL');
|
|
}
|
|
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
throw new Error('only http(s) URLs are allowed');
|
|
}
|
|
return url;
|
|
}
|
|
|
|
function assertPublicHttpUrl(raw) {
|
|
const url = assertHttpUrl(raw);
|
|
if (isBlockedHostname(url.hostname)) {
|
|
throw new Error('private, loopback, and metadata hosts are blocked');
|
|
}
|
|
return url;
|
|
}
|
|
|
|
module.exports = { isBlockedHostname, assertHttpUrl, assertPublicHttpUrl };
|