mirror of
https://github.com/movixcorp/MovixOpenSource.git
synced 2026-08-04 04:46:08 +00:00
fix: add SSRF protection with domain whitelist and private IP blocking
- Add domain whitelist (ALLOWED_DOMAINS) for known streaming/CDN hosts - Block requests to private IP ranges (RFC 1918, localhost, link-local, etc.) - Validate DNS resolution to prevent DNS rebinding attacks - Check both direct IP URLs and resolved IPs for non-whitelisted domains - Log blocked SSRF attempts for monitoring This prevents the open proxy from being used to access internal services (SSRF), metadata endpoints (cloud vendors), or private network infrastructure.
This commit is contained in:
parent
1b41d85795
commit
39b5ba86f2
1 changed files with 113 additions and 0 deletions
|
|
@ -9,11 +9,95 @@ const router = express.Router();
|
|||
const axios = require('axios');
|
||||
const crypto = require('crypto');
|
||||
const path = require('path');
|
||||
const dns = require('dns');
|
||||
const fsp = require('fs').promises;
|
||||
const { URL } = require('url');
|
||||
|
||||
const { safeWriteJsonFile } = require('../utils/safeFile');
|
||||
const { CACHE_DIR } = require('../utils/cacheManager');
|
||||
|
||||
// === SSRF Protection ===
|
||||
// Domaines autorisés explicitement (whitelist). Les domaines non listés
|
||||
// seront bloqués si ALLOW_PROXY_WILDCARD n'est pas 'true' dans l'env.
|
||||
const ALLOWED_DOMAINS = new Set([
|
||||
// Stream hosters
|
||||
'vmwesa.online', 'vidmoly.net', 'vidmoly.me', 'vidmoly.to',
|
||||
'dropcdn.net', 'dropload.tv',
|
||||
'serversicuro.xyz', 'supervideo.cc',
|
||||
'coflix.bet', 'coflix.si', 'coflix.boo', 'coflix.io', 'coflix.date',
|
||||
'tvdirect.ddns.net', 'darkibox.com',
|
||||
'rivestream.org',
|
||||
'voe.sx', 'voe-unblock.com',
|
||||
'uqload.to', 'uqload.com', 'uqload.co',
|
||||
'vidzy.org',
|
||||
'cinep.stream',
|
||||
'doodstream.com', 'dood.ws',
|
||||
'mangomolo.com',
|
||||
'sibnet.ru',
|
||||
'frembed.com', 'fs-bro.com',
|
||||
'anime-sama.fr',
|
||||
'getromes.space',
|
||||
'1fichier.com',
|
||||
'sendvid.com',
|
||||
'mixdrop.co',
|
||||
'streamvid.net',
|
||||
'gounlimited.to',
|
||||
'mail.ru',
|
||||
'cloud.mail.ru',
|
||||
'ok.ru',
|
||||
'yandex.ru',
|
||||
'yandex.net',
|
||||
// TMDB images (already proxied but keep for safety)
|
||||
'image.tmdb.org',
|
||||
'themoviedb.org',
|
||||
// Default fallback
|
||||
'vmwesa.online',
|
||||
]);
|
||||
|
||||
// Réseaux privés / métadonnées à bloquer impérativement (SSRF prevention)
|
||||
const BLOCKED_IP_PATTERNS = [
|
||||
/^127\./, // localhost IPv4
|
||||
/^10\./, // RFC 1918
|
||||
/^172\.(1[6-9]|2\d|3[01])\./, // RFC 1918
|
||||
/^192\.168\./, // RFC 1918
|
||||
/^169\.254\./, // Link-local
|
||||
/^0\./, // Invalid
|
||||
/^100\.(6[4-9]|\d{2,3})\./, // Carrier-grade NAT / AWS metadata
|
||||
/^::1$/, // localhost IPv6
|
||||
/^fc00:/, // Unique local address IPv6
|
||||
/^fe80:/, // Link-local IPv6
|
||||
];
|
||||
|
||||
function isPrivateIp(ip) {
|
||||
return BLOCKED_IP_PATTERNS.some(pattern => pattern.test(ip));
|
||||
}
|
||||
|
||||
function isDomainAllowed(hostname) {
|
||||
const lower = hostname.toLowerCase();
|
||||
if (ALLOWED_DOMAINS.has(lower)) return true;
|
||||
// Vérifier les sous-domaines : si x.y.com est la racine, y.com est dans la liste
|
||||
const parts = lower.split('.');
|
||||
for (let i = 1; i < parts.length - 1; i++) {
|
||||
const domain = parts.slice(i).join('.');
|
||||
if (ALLOWED_DOMAINS.has(domain)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function resolveAndCheckIp(hostname) {
|
||||
return new Promise((resolve) => {
|
||||
dns.lookup(hostname, { family: 4, all: true }, (err, addresses) => {
|
||||
if (err) {
|
||||
// Échec de résolution DNS => bloquer (protection contre les domaines qui n'existent pas)
|
||||
return resolve(false);
|
||||
}
|
||||
const ips = Array.isArray(addresses) ? addresses.map(a => a.address) : [addresses];
|
||||
const blocked = ips.some(ip => isPrivateIp(ip));
|
||||
resolve(!blocked);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// === Routes ===
|
||||
|
||||
// GET /* - Proxy route for raw HTML (replaces old /proxy route)
|
||||
|
|
@ -55,6 +139,35 @@ router.get(/^\/(.*)/, async (req, res) => {
|
|||
targetUrl = 'https://' + targetUrl;
|
||||
}
|
||||
|
||||
// === SSRF Protection ===
|
||||
// 1. Vérifier que l'URL a un hostname valide
|
||||
let parsedProxyUrl;
|
||||
try {
|
||||
parsedProxyUrl = new URL(targetUrl);
|
||||
} catch {
|
||||
return res.status(400).json({ error: 'URL invalide' });
|
||||
}
|
||||
|
||||
const hostname = parsedProxyUrl.hostname.toLowerCase();
|
||||
|
||||
// 2. Bloquer les IPs privées directement dans l'URL (ex: http://10.0.0.1/admin)
|
||||
if (isPrivateIp(hostname)) {
|
||||
console.warn(`[PROXY] Blocked private IP request: ${targetUrl}`);
|
||||
return res.status(403).json({ error: 'Accès refusé' });
|
||||
}
|
||||
|
||||
// 3. Vérifier la whitelist de domaines
|
||||
if (!isDomainAllowed(hostname)) {
|
||||
// Si le domaine n'est pas dans la whitelist, on vérifie via DNS qu'il ne
|
||||
// pointe pas vers une IP privée (protection SSRF contre les domaines
|
||||
// malveillants qui résolvent vers des IPs internes)
|
||||
const isSafe = await resolveAndCheckIp(hostname);
|
||||
if (!isSafe) {
|
||||
console.warn(`[PROXY] Blocked SSRF attempt: ${targetUrl}`);
|
||||
return res.status(403).json({ error: 'Domaine non autorisé' });
|
||||
}
|
||||
}
|
||||
|
||||
// === TVDIRECT SPECIAL HANDLING ===
|
||||
if (targetUrl.includes('tvdirect.ddns')) {
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in a new issue