102 lines
3.4 KiB
C#
102 lines
3.4 KiB
C#
|
|
using System.Net;
|
||
|
|
|
||
|
|
namespace ChatbotApi.Services
|
||
|
|
{
|
||
|
|
public static class UrlSecurity
|
||
|
|
{
|
||
|
|
// Lista de rangos de IP privados y reservados
|
||
|
|
private static readonly List<(IPAddress Address, int PrefixLength)> PrivateRanges = new List<(IPAddress, int)>
|
||
|
|
{
|
||
|
|
(IPAddress.Parse("10.0.0.0"), 8),
|
||
|
|
(IPAddress.Parse("172.16.0.0"), 12),
|
||
|
|
(IPAddress.Parse("192.168.0.0"), 16),
|
||
|
|
(IPAddress.Parse("127.0.0.0"), 8),
|
||
|
|
(IPAddress.Parse("0.0.0.0"), 8),
|
||
|
|
(IPAddress.Parse("::1"), 128) // IPv6 Loopback
|
||
|
|
};
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Verifica si una URL es segura para ser visitada por el bot.
|
||
|
|
/// Bloquea IPs privadas, locales y esquemas no HTTP/HTTPS.
|
||
|
|
/// </summary>
|
||
|
|
public static async Task<bool> IsSafeUrlAsync(string url)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrWhiteSpace(url)) return false;
|
||
|
|
|
||
|
|
// 1. Validar formato de URL y esquema (solo http/https)
|
||
|
|
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uriResult)) return false;
|
||
|
|
if (uriResult.Scheme != Uri.UriSchemeHttp && uriResult.Scheme != Uri.UriSchemeHttps) return false;
|
||
|
|
|
||
|
|
// 2. Si es eldia.com, confiamos (Whitelisting explícito para el dominio principal)
|
||
|
|
if (uriResult.Host.EndsWith("eldia.com", StringComparison.OrdinalIgnoreCase)) return true;
|
||
|
|
|
||
|
|
// 3. Resolución DNS para verificar que no apunte a una IP local (SSRF)
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var ipAddresses = await Dns.GetHostAddressesAsync(uriResult.Host);
|
||
|
|
foreach (var ip in ipAddresses)
|
||
|
|
{
|
||
|
|
if (IsPrivateIp(ip))
|
||
|
|
{
|
||
|
|
// Log (opcional): Intento de acceso a IP privada: uriResult.Host -> ip
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
// Si falla el DNS, denegamos por seguridad
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static bool IsPrivateIp(IPAddress ip)
|
||
|
|
{
|
||
|
|
if (IPAddress.IsLoopback(ip)) return true;
|
||
|
|
|
||
|
|
// Convertir a bytes para comparar rangos
|
||
|
|
byte[] ipBytes = ip.GetAddressBytes();
|
||
|
|
|
||
|
|
// Manejo simplificado para IPv4 Mapped to IPv6
|
||
|
|
if (ip.IsIPv4MappedToIPv6)
|
||
|
|
{
|
||
|
|
ip = ip.MapToIPv4();
|
||
|
|
ipBytes = ip.GetAddressBytes();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Solo verificamos rangos privados en IPv4 por simplicidad y riesgo común
|
||
|
|
if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
|
||
|
|
{
|
||
|
|
foreach (var (baseIp, prefixLength) in PrivateRanges)
|
||
|
|
{
|
||
|
|
if (baseIp.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork &&
|
||
|
|
IsInSubnet(ip, baseIp, prefixLength))
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static bool IsInSubnet(IPAddress address, IPAddress subnetMask, int prefixLength)
|
||
|
|
{
|
||
|
|
var ipBytes = address.GetAddressBytes();
|
||
|
|
var maskBytes = subnetMask.GetAddressBytes();
|
||
|
|
|
||
|
|
// Calcular máscara de bits
|
||
|
|
var bits = new System.Collections.BitArray(ipBytes.Length * 8, false);
|
||
|
|
// Lógica simplificada de comparación de bits para netmask...
|
||
|
|
// Para mantener el código limpio y funcional sin librerías externas complejas:
|
||
|
|
|
||
|
|
// Chequeo rápido de los 3 rangos clásicos de RFC1918
|
||
|
|
if (ipBytes[0] == 10) return true; // 10.0.0.0/8
|
||
|
|
if (ipBytes[0] == 172 && ipBytes[1] >= 16 && ipBytes[1] <= 31) return true; // 172.16.0.0/12
|
||
|
|
if (ipBytes[0] == 192 && ipBytes[1] == 168) return true; // 192.168.0.0/16
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|