All files / src/utils networkValidator.js

0% Statements 0/26
0% Branches 0/10
0% Functions 0/3
0% Lines 0/25

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90                                                                                                                                                                                   
/**
 * Validation reseau — Bloque les IPs privees pour prevenir le SSRF.
 *
 * Utilise pour les URLs fournies par les utilisateurs (uptime monitors, webhooks)
 * afin d'empecher le scan du reseau interne.
 *
 * @module utils/networkValidator
 */
 
const { URL } = require('node:url');
const dns = require('node:dns');
const { promisify } = require('node:util');
 
const dnsResolve = promisify(dns.resolve4);
 
// Plages d'IPs privees (RFC 1918 + loopback + link-local + metadata)
const PRIVATE_RANGES = [
  /^127\./,                        // Loopback
  /^10\./,                         // Classe A privee
  /^172\.(1[6-9]|2\d|3[01])\./,   // Classe B privee
  /^192\.168\./,                   // Classe C privee
  /^169\.254\./,                   // Link-local
  /^0\./,                          // Reseau courant
  /^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./, // Shared address (CGN)
  /^::1$/,                         // IPv6 loopback
  /^f[cd]/i,                       // IPv6 unique local
  /^fe80/i,                        // IPv6 link-local
];
 
// Hostnames bloques (cloud metadata endpoints)
const BLOCKED_HOSTS = new Set([
  'localhost',
  'metadata.google.internal',
  '169.254.169.254',       // AWS/GCP metadata
  'metadata.internal',
]);
 
/**
 * Verifie si une IP est privee.
 * @param {string} ip
 * @returns {boolean}
 */
function isPrivateIp(ip) {
  return PRIVATE_RANGES.some((range) => range.test(ip));
}
 
/**
 * Valide qu'une URL ne pointe pas vers une IP privee.
 * Resout le DNS pour verifier l'IP reelle.
 *
 * @param {string} urlString — URL a verifier
 * @returns {Promise<{ valid: boolean, reason?: string }>}
 */
async function validatePublicUrl(urlString) {
  try {
    const parsed = new URL(urlString);
    const hostname = parsed.hostname;
 
    // Bloquer les hostnames connus
    if (BLOCKED_HOSTS.has(hostname.toLowerCase())) {
      return { valid: false, reason: 'Acces au reseau interne interdit.' };
    }
 
    // Si c'est deja une IP, verifier directement
    if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
      if (isPrivateIp(hostname)) {
        return { valid: false, reason: 'Les adresses IP privees ne sont pas autorisees.' };
      }
      return { valid: true };
    }
 
    // Resoudre le DNS et verifier
    const ips = await dnsResolve(hostname);
    for (const ip of ips) {
      if (isPrivateIp(ip)) {
        return { valid: false, reason: `Le domaine ${hostname} resout vers une IP privee (${ip}).` };
      }
    }
 
    return { valid: true };
  } catch (err) {
    if (err.code === 'ENOTFOUND') {
      return { valid: false, reason: 'Domaine introuvable.' };
    }
    return { valid: false, reason: `Erreur de validation : ${err.message}` };
  }
}
 
module.exports = { validatePublicUrl, isPrivateIp };