wa-dev-tools/lib/ssrf.js
treamz 5b528d243a Refactor: modular architecture, security, config
- Split monolithic server.js (1111 lines) into route modules
- Add .env config (port, session secret, quality, limits)
- Add SSRF protection for parser/proxy/redirect endpoints
- Add optional password auth middleware
- Add structured logger (replaces raw fs.appendFileSync)
- Add graceful shutdown with timeout
- Add extended /health endpoint (uptime, memory, pid)
- Add ecosystem.config.js for PM2 (memory limit, restart policy)
- Make compress quality configurable (was hardcoded 60)
- Expand SVG AI icons library (7 -> 20 icons)
- Add dotenv dependency
2026-03-21 22:18:54 +03:00

81 lines
2.1 KiB
JavaScript

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 };