Initial commit
This commit is contained in:
Executable
+80
@@ -0,0 +1,80 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user