const { URL } = require('url'); const dns = require('dns'); const net = require('net'); const BLOCKED_RANGES = [ // IPv4 private /^10\./, /^172\.(1[6-9]|2\d|3[01])\./, /^192\.168\./, /^127\./, /^0\./, // Link-local /^169\.254\./, // Loopback IPv6 /^::1$/, /^fc/i, /^fd/i, /^fe80/i, ]; function isPrivateIP(ip) { return BLOCKED_RANGES.some(re => re.test(ip)); } /** * Validate URL is safe for server-side requests (no SSRF) * Returns { safe: true, url } or { safe: false, error } */ async function validateUrl(rawUrl) { if (!rawUrl || typeof rawUrl !== 'string') { return { safe: false, error: 'URL обязателен' }; } let parsed; try { parsed = new URL(rawUrl); } catch { return { safe: false, error: 'Некорректный URL' }; } if (!['http:', 'https:'].includes(parsed.protocol)) { return { safe: false, error: 'Поддерживаются только http и https' }; } const hostname = parsed.hostname; // Block direct IP access to private ranges if (net.isIP(hostname)) { if (isPrivateIP(hostname)) { return { safe: false, error: 'Доступ к внутренним адресам запрещён' }; } return { safe: true, url: rawUrl }; } // Block common internal hostnames const lower = hostname.toLowerCase(); if (lower === 'localhost' || lower.endsWith('.local') || lower.endsWith('.internal')) { return { safe: false, error: 'Доступ к внутренним адресам запрещён' }; } // DNS resolve and check IP return new Promise((resolve) => { dns.resolve4(hostname, (err, addresses) => { if (err || !addresses || addresses.length === 0) { // Allow through — DNS might fail but fetch will handle it resolve({ safe: true, url: rawUrl }); return; } for (const addr of addresses) { if (isPrivateIP(addr)) { resolve({ safe: false, error: 'Доступ к внутренним адресам запрещён' }); return; } } resolve({ safe: true, url: rawUrl }); }); }); } module.exports = { validateUrl, isPrivateIP };