Files
Chatbot-ElDia/ChatbotApi/Services/UrlSecurity.cs

102 lines
3.4 KiB
C#
Raw Normal View History

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