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:
2025-11-27 15:11:54 -03:00
parent 6f96ca9c79
commit 67e179441d
8 changed files with 539 additions and 443 deletions

View File

@@ -7,46 +7,64 @@ using System.Text;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Identity;
using Microsoft.OpenApi.Models;
using Microsoft.EntityFrameworkCore; // Necesario para UseSqlServer
// Cargar variables de entorno desde el archivo .env
// Cargar variables de entorno
Env.Load();
var builder = WebApplication.CreateBuilder(args);
// Definimos una política de CORS para permitir solicitudes desde nuestro frontend de Vite
// [SEGURIDAD] Configuración de Kestrel para ocultar el header "Server" (Information Disclosure)
builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.AddServerHeader = false;
});
// [SEGURIDAD] CORS Restrictivo
var myAllowSpecificOrigins = "_myAllowSpecificOrigins";
builder.Services.AddCors(options =>
{
options.AddPolicy(name: myAllowSpecificOrigins,
policy =>
{
policy.WithOrigins("192.168.10.78", "http://192.168.5.129:8081", "http://192.168.5.129:8082", "http://localhost:5173", "http://localhost:5174")
policy.WithOrigins(
"http://192.168.5.129:8081",
"http://192.168.5.129:8082",
"http://localhost:5173",
"http://localhost:5174",
"http://localhost:5175")
.AllowAnyHeader()
.AllowAnyMethod();
// [SEGURIDAD] Solo permitimos los verbos necesarios. Bloqueamos TRACE, HEAD, etc.
.WithMethods("GET", "POST", "PUT", "DELETE", "OPTIONS");
});
});
// 1. Añadimos el DbContext para Entity Framework
// 1. DbContext
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<AppContexto>(options =>
options.UseSqlServer(connectionString));
// 2. Añadimos ASP.NET Core Identity
// 2. Identity
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
{
// [SEGURIDAD] Políticas de contraseñas robustas
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequiredLength = 12; // Aumentado de 8 a 12
options.Password.RequireNonAlphanumeric = true; // Requerimos símbolos
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = false;
options.Password.RequireLowercase = true;
// [SEGURIDAD] Bloqueo de cuenta tras intentos fallidos (Mitigación Fuerza Bruta)
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
})
.AddEntityFrameworkStores<AppContexto>()
.AddDefaultTokenProviders();
builder.Services.AddMemoryCache();
// =========== INICIO DE CONFIGURACIÓN JWT ===========
// JWT Config
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
@@ -64,22 +82,31 @@ builder.Services.AddAuthentication(options =>
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException("La clave JWT no está configurada.")
))
)),
ClockSkew = TimeSpan.Zero // Token expira exactamente cuando dice
};
});
// RATE LIMITING
// [SEGURIDAD] RATE LIMITING AVANZADO
builder.Services.AddRateLimiter(options =>
{
// Política General: 30 peticiones por minuto (Suficiente para uso normal del chat)
options.AddFixedWindowLimiter(policyName: "fixed", limiterOptions =>
{
limiterOptions.PermitLimit = 10; // Permitir 10 peticiones...
limiterOptions.Window = TimeSpan.FromMinutes(1); // ...por cada minuto.
limiterOptions.QueueLimit = 2; // Poner en cola hasta 2 peticiones si se excede el límite brevemente.
limiterOptions.PermitLimit = 30;
limiterOptions.Window = TimeSpan.FromMinutes(1);
limiterOptions.QueueLimit = 2;
limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
});
// Esta función se ejecuta cuando una petición es rechazada
// [SEGURIDAD] Política Estricta para Login: 5 intentos por minuto (Anti Fuerza Bruta)
options.AddFixedWindowLimiter(policyName: "login-limit", limiterOptions =>
{
limiterOptions.PermitLimit = 5;
limiterOptions.Window = TimeSpan.FromMinutes(1);
limiterOptions.QueueLimit = 0; // No encolar, rechazar directo
});
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
});
@@ -92,22 +119,17 @@ builder.Services.AddSwaggerGen(options =>
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "Por favor, introduce 'Bearer' seguido de un espacio y el token JWT",
Description = "JWT Authorization header using the Bearer scheme.",
Name = "Authorization",
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
Type = SecuritySchemeType.Http,
Scheme = "bearer"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }
},
new string[] {}
}
@@ -116,29 +138,52 @@ builder.Services.AddSwaggerGen(options =>
var app = builder.Build();
// [SEGURIDAD] Headers de Seguridad HTTP
// Se recomienda usar la librería 'NetEscapades.AspNetCore.SecurityHeaders'
// Si no la tienes, instálala: dotnet add package NetEscapades.AspNetCore.SecurityHeaders
app.UseSecurityHeaders(policy =>
{
policy.AddDefaultSecurityHeaders();
policy.AddStrictTransportSecurityMaxAgeIncludeSubDomains(maxAgeInSeconds: 60 * 60 * 24 * 365); // HSTS 1 año
policy.AddContentSecurityPolicy(builder =>
{
builder.AddDefaultSrc().Self();
// Permitimos scripts inline solo si es estrictamente necesario para Swagger, idealmente usar nonces
builder.AddScriptSrc().Self().UnsafeInline();
builder.AddStyleSrc().Self().UnsafeInline();
builder.AddImgSrc().Self().Data();
builder.AddConnectSrc().Self()
.From("http://localhost:5173")
.From("http://localhost:5174")
.From("http://localhost:5175"); // Permitir conexiones explícitas
builder.AddFrameAncestors().None(); // Previene Clickjacking
});
policy.RemoveServerHeader(); // Capa extra para ocultar Kestrel
});
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
else
{
// [SEGURIDAD] Forzar HTTPS en producción
app.UseHsts();
}
app.UseHttpsRedirection();
// =========== CONFIGURACIÓN Y USO DEL MIDDLEWARE DE ENCABEZADOS DE SEGURIDAD ===========
app.UseSecurityHeaders(policy =>
{
policy.AddDefaultSecurityHeaders(); // Añade los encabezados por defecto
policy.AddContentSecurityPolicy(builder =>
{
builder.AddDefaultSrc().Self();
// Permisos necesarios para Swagger UI
builder.AddScriptSrc().Self().UnsafeInline();
builder.AddStyleSrc().Self().UnsafeInline();
});
});
app.UseCors(myAllowSpecificOrigins);
// [SEGURIDAD] Aplicar Rate Limiting
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
// Mapeamos los controladores. Las políticas de Rate Limiting se aplicarán vía Atributos en cada Controller.
app.MapControllers();
app.Run();