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.
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
// Controllers/AdminController.cs
|
||||
using ChatbotApi.Data.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using ChatbotApi.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ChatbotApi.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
[Authorize] // Requiere Token JWT válido
|
||||
public class AdminController : ControllerBase
|
||||
{
|
||||
private readonly AppContexto _context;
|
||||
@@ -21,7 +21,8 @@ namespace ChatbotApi.Controllers
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
// GET: api/admin/contexto
|
||||
// --- CONTEXTO ITEMS (Sin cambios mayores de seguridad más allá de Authorize) ---
|
||||
|
||||
[HttpGet("contexto")]
|
||||
public async Task<IActionResult> GetAllContextoItems()
|
||||
{
|
||||
@@ -29,10 +30,12 @@ namespace ChatbotApi.Controllers
|
||||
return Ok(items);
|
||||
}
|
||||
|
||||
// POST: api/admin/contexto
|
||||
[HttpPost("contexto")]
|
||||
public async Task<IActionResult> CreateContextoItem([FromBody] ContextoItem item)
|
||||
{
|
||||
// [SEGURIDAD] Validación de entrada
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
|
||||
if (await _context.ContextoItems.AnyAsync(i => i.Clave == item.Clave))
|
||||
{
|
||||
return BadRequest("La clave ya existe.");
|
||||
@@ -40,60 +43,42 @@ namespace ChatbotApi.Controllers
|
||||
item.FechaActualizacion = DateTime.UtcNow;
|
||||
_context.ContextoItems.Add(item);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Invalida la caché de KnowledgeItems
|
||||
_cache.Remove(CacheKeys.KnowledgeItems);
|
||||
|
||||
return CreatedAtAction(nameof(GetAllContextoItems), new { id = item.Id }, item);
|
||||
}
|
||||
|
||||
// PUT: api/admin/contexto/5
|
||||
[HttpPut("contexto/{id}")]
|
||||
public async Task<IActionResult> UpdateContextoItem(int id, [FromBody] ContextoItem item)
|
||||
{
|
||||
if (id != item.Id)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
if (id != item.Id) return BadRequest();
|
||||
|
||||
var existingItem = await _context.ContextoItems.FindAsync(id);
|
||||
if (existingItem == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
if (existingItem == null) return NotFound();
|
||||
|
||||
existingItem.Valor = item.Valor;
|
||||
existingItem.Descripcion = item.Descripcion;
|
||||
existingItem.FechaActualizacion = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
// Invalida la caché de KnowledgeItems
|
||||
_cache.Remove(CacheKeys.KnowledgeItems);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// DELETE: api/admin/contexto/5
|
||||
[HttpDelete("contexto/{id}")]
|
||||
public async Task<IActionResult> DeleteContextoItem(int id)
|
||||
{
|
||||
var item = await _context.ContextoItems.FindAsync(id);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
if (item == null) return NotFound();
|
||||
_context.ContextoItems.Remove(item);
|
||||
await _context.SaveChangesAsync();
|
||||
// Invalida la caché de KnowledgeItems
|
||||
_cache.Remove(CacheKeys.KnowledgeItems);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// GET: api/admin/logs
|
||||
[HttpGet("logs")]
|
||||
public async Task<IActionResult> GetConversationLogs()
|
||||
{
|
||||
// Obtenemos los últimos 200 logs, ordenados del más reciente al más antiguo.
|
||||
// Limitamos a 200 para evitar sobrecarga
|
||||
var logs = await _context.ConversacionLogs
|
||||
.OrderByDescending(log => log.Fecha)
|
||||
.Take(200)
|
||||
@@ -101,7 +86,8 @@ namespace ChatbotApi.Controllers
|
||||
return Ok(logs);
|
||||
}
|
||||
|
||||
// ENDPOINTS PARA FUENTES DE CONTEXTO (URLs)
|
||||
// --- FUENTES DE CONTEXTO (APLICAMOS LA SEGURIDAD SSRF) ---
|
||||
|
||||
[HttpGet("fuentes")]
|
||||
public async Task<IActionResult> GetAllFuentes()
|
||||
{
|
||||
@@ -112,10 +98,16 @@ namespace ChatbotApi.Controllers
|
||||
[HttpPost("fuentes")]
|
||||
public async Task<IActionResult> CreateFuente([FromBody] FuenteContexto fuente)
|
||||
{
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
|
||||
// [SEGURIDAD] Validar que la URL no sea interna/maliciosa ANTES de guardarla
|
||||
if (!await UrlSecurity.IsSafeUrlAsync(fuente.Url))
|
||||
{
|
||||
return BadRequest("La URL proporcionada no es válida o apunta a una dirección interna restringida.");
|
||||
}
|
||||
|
||||
_context.FuentesDeContexto.Add(fuente);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Invalida la caché de FuentesDeContexto
|
||||
_cache.Remove(CacheKeys.FuentesDeContexto);
|
||||
|
||||
return CreatedAtAction(nameof(GetAllFuentes), new { id = fuente.Id }, fuente);
|
||||
@@ -124,9 +116,12 @@ namespace ChatbotApi.Controllers
|
||||
[HttpPut("fuentes/{id}")]
|
||||
public async Task<IActionResult> UpdateFuente(int id, [FromBody] FuenteContexto fuente)
|
||||
{
|
||||
if (id != fuente.Id)
|
||||
if (id != fuente.Id) return BadRequest();
|
||||
|
||||
// [SEGURIDAD] Validar también en la actualización
|
||||
if (!await UrlSecurity.IsSafeUrlAsync(fuente.Url))
|
||||
{
|
||||
return BadRequest();
|
||||
return BadRequest("La URL proporcionada no es válida o apunta a una dirección interna restringida.");
|
||||
}
|
||||
|
||||
_context.Entry(fuente).State = EntityState.Modified;
|
||||
@@ -137,19 +132,11 @@ namespace ChatbotApi.Controllers
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
if (!_context.FuentesDeContexto.Any(e => e.Id == id))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw;
|
||||
}
|
||||
if (!_context.FuentesDeContexto.Any(e => e.Id == id)) return NotFound();
|
||||
else throw;
|
||||
}
|
||||
|
||||
// Invalida la caché de FuentesDeContexto
|
||||
_cache.Remove(CacheKeys.FuentesDeContexto);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -157,17 +144,11 @@ namespace ChatbotApi.Controllers
|
||||
public async Task<IActionResult> DeleteFuente(int id)
|
||||
{
|
||||
var fuente = await _context.FuentesDeContexto.FindAsync(id);
|
||||
if (fuente == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
if (fuente == null) return NotFound();
|
||||
|
||||
_context.FuentesDeContexto.Remove(fuente);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Invalida la caché de FuentesDeContexto
|
||||
_cache.Remove(CacheKeys.FuentesDeContexto);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user