81 lines
2.1 KiB
JavaScript
Executable File
81 lines
2.1 KiB
JavaScript
Executable File
import dns from 'node:dns/promises';
|
|
import net from 'node:net';
|
|
|
|
// Värdnamn som medvetet tillåts trots att de pekar lokalt (t.ex. lokal Ollama).
|
|
export const LOCAL_ALLOWED_HOSTS = new Set([
|
|
'host.docker.internal',
|
|
'localhost',
|
|
'127.0.0.1',
|
|
'::1',
|
|
]);
|
|
|
|
function isPrivateIp(ip) {
|
|
if (net.isIPv4(ip)) {
|
|
const [a, b] = ip.split('.').map(Number);
|
|
return (
|
|
a === 10 ||
|
|
a === 127 ||
|
|
a === 0 ||
|
|
(a === 192 && b === 168) ||
|
|
(a === 172 && b >= 16 && b <= 31) ||
|
|
(a === 169 && b === 254) // link-local / cloud-metadata
|
|
);
|
|
}
|
|
if (net.isIPv6(ip)) {
|
|
const lower = ip.toLowerCase();
|
|
return (
|
|
lower === '::1' ||
|
|
lower === '::' ||
|
|
lower.startsWith('fc') ||
|
|
lower.startsWith('fd') ||
|
|
lower.startsWith('fe80') ||
|
|
lower.startsWith('::ffff:') // IPv4-mappad
|
|
);
|
|
}
|
|
return true; // okänt format → blockera
|
|
}
|
|
|
|
/**
|
|
* Validerar en URL mot SSRF. Kastar om adressen är ogiltig eller pekar internt.
|
|
* @param {string} rawUrl
|
|
* @param {{ allowList?: Set<string>, allowPrivateNetwork?: boolean }} [options] - värdnamn som tillåts trots privat IP
|
|
* @returns {Promise<URL>}
|
|
*/
|
|
export async function assertSafeUrl(rawUrl, { allowList = new Set(), allowPrivateNetwork = false } = {}) {
|
|
let url;
|
|
try {
|
|
url = new URL(String(rawUrl || ''));
|
|
} catch {
|
|
throw new Error('Ogiltig URL');
|
|
}
|
|
|
|
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
throw new Error('Endast http/https tillåts');
|
|
}
|
|
|
|
if (allowList.has(url.hostname)) return url;
|
|
|
|
// Om värden redan är en literal IP behöver vi ingen DNS-lookup.
|
|
if (net.isIP(url.hostname)) {
|
|
if (!allowPrivateNetwork && isPrivateIp(url.hostname)) throw new Error('Adressen pekar mot ett internt nät');
|
|
return url;
|
|
}
|
|
|
|
let resolved;
|
|
try {
|
|
resolved = await dns.lookup(url.hostname, { all: true });
|
|
} catch {
|
|
throw new Error('Kunde inte slå upp värdnamnet');
|
|
}
|
|
|
|
if (!resolved.length) {
|
|
throw new Error('Kunde inte slå upp värdnamnet');
|
|
}
|
|
|
|
if (!allowPrivateNetwork && resolved.some(entry => isPrivateIp(entry.address))) {
|
|
throw new Error('Adressen pekar mot ett internt nät');
|
|
}
|
|
|
|
return url;
|
|
}
|