Files
Chatbot-ElDia/ChatbotApi/Services/UrlSecurity.cs
dmolinari 67e179441d feat: Añadidos de seguridad (Backend, Frontend e IA)
Implementación de medidas de seguridad críticas tras auditoría:

Backend (API & IA):
- Anti-Prompt Injection: Reestructuración de prompts con delimitadores XML y sanitización estricta de inputs (Tag Injection).
- Anti-SSRF: Implementación de servicio `UrlSecurity` para validar URLs y bloquear accesos a IPs internas/privadas en funciones de scraping.
- Moderación: Activación de `SafetySettings` en Gemini API.
- Infraestructura:
  - Configuración de Headers de seguridad (HSTS, CSP, NoSniff).
  - CORS restrictivo (solo métodos HTTP necesarios).
  - Rate Limiting global y política estricta para Login (5 req/min).
  - Timeouts en HttpClient para prevenir DoS.
- Auth: Endpoint `setup-admin` restringido exclusivamente a entorno Debug.

Frontend (React):
- Anti-XSS & Tabnabbing: Configuración de esquema estricto en `rehype-sanitize` y forzado de `rel="noopener noreferrer"` en enlaces.
- Validación de longitud de input en cliente.

IA:
- Se realiza afinación de contexto de preguntas.
2025-11-27 15:11:54 -03:00

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