Auditoría de Seguridad - API Nexus Platform¶
Fecha: 2026-01-30 Versión: 1.0 Estado: CRÍTICO - Requiere acción inmediata Alcance: Orchestrator.Api, Orchestrator.Mcp.Remote, Hubs, Webhooks
Índice¶
- Resumen Ejecutivo
- Hallazgos Críticos
- Análisis por Componente
- Matriz de Vulnerabilidades
- Plan de Securización
- Implementación Detallada
- Checklist de Seguridad
Resumen Ejecutivo¶
Estado Actual: 🔴 CRÍTICO¶
La API de Nexus Platform presenta vulnerabilidades críticas de seguridad que requieren atención inmediata:
| Categoría | Crítico | Alto | Medio | Bajo |
|---|---|---|---|---|
| Autenticación | 3 | 2 | 2 | 0 |
| Autorización | 2 | 4 | 3 | 1 |
| Validación de Input | 1 | 3 | 4 | 2 |
| Webhooks | 2 | 1 | 2 | 0 |
| Secretos | 3 | 2 | 2 | 0 |
| TOTAL | 11 | 12 | 13 | 3 |
Hallazgos Principales¶
┌─────────────────────────────────────────────────────────────┐
│ PROBLEMAS CRÍTICOS IDENTIFICADOS │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. SIN AUTENTICACIÓN GLOBAL EN LA API │
│ - No hay app.UseAuthentication() │
│ - No hay app.UseAuthorization() │
│ - 38 de 39 controllers sin [Authorize] │
│ │
│ 2. HEADER X-User-Id PUEDE SER SUPLANTADO │
│ - Cualquier cliente puede enviar cualquier UserId │
│ - Sin validación contra token JWT │
│ - Permite escalada de privilegios │
│ │
│ 3. SECRETOS EXPUESTOS EN REPOSITORIO │
│ - API keys de Claude, ClickUp, Holded │
│ - Credenciales SMTP │
│ - Claves de encriptación │
│ │
│ 4. WEBHOOKS SIN VALIDACIÓN DE FIRMA │
│ - UltraMsg: SIN validación │
│ - Sentry/ClickUp: Validación OPCIONAL │
│ │
│ 5. TÚNELES PERMITEN EJECUCIÓN REMOTA SIN AUTH │
│ - TunnelEndpoints completamente abiertos │
│ - Cualquiera puede ejecutar comandos remotos │
│ │
└─────────────────────────────────────────────────────────────┘
Hallazgos Críticos¶
CRIT-001: Sin Autenticación Global en API¶
Severidad: 🔴 CRÍTICA
Archivo: Orchestrator/src/Orchestrator.Api/Program.cs
Problema: La API no tiene configuración de autenticación ni autorización.
// ❌ FALTA en Program.cs:
// builder.Services.AddAuthentication(...)
// builder.Services.AddAuthorization(...)
// app.UseAuthentication()
// app.UseAuthorization()
Impacto: Todos los endpoints están accesibles sin autenticación.
Solución:
// Program.cs - AGREGAR:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
};
});
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
// En el pipeline:
app.UseAuthentication();
app.UseAuthorization();
CRIT-002: Header X-User-Id Suplantable¶
Severidad: 🔴 CRÍTICA Archivos: 15+ Controllers
Problema: El UserId se obtiene de un header HTTP que el cliente puede manipular.
// ❌ CÓDIGO ACTUAL (INSEGURO):
private Guid? GetUserId()
{
// El cliente puede enviar cualquier UserId
var headerUserId = Request.Headers["X-User-Id"].FirstOrDefault();
if (Guid.TryParse(headerUserId, out var headerGuid))
return headerGuid;
// Solo como fallback usa claims
var claim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (Guid.TryParse(claim, out var claimGuid))
return claimGuid;
return null;
}
Ataque:
# Un atacante puede suplantar a cualquier usuario:
curl -H "X-User-Id: admin-user-guid-here" \
http://api.nexus.local/api/projects
# Accede a todos los datos del usuario suplantado
Solución:
// ✅ CÓDIGO SEGURO:
private Guid GetUserId()
{
// SOLO usar claims del token JWT validado
var claim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value
?? throw new UnauthorizedAccessException("User not authenticated");
if (!Guid.TryParse(claim, out var userId))
throw new UnauthorizedAccessException("Invalid user claim");
return userId;
}
// Para servicios internos (service-to-service):
private Guid? GetUserIdWithServiceFallback()
{
// Primero intentar claims del usuario
var claim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (Guid.TryParse(claim, out var userId))
return userId;
// Solo permitir X-User-Id si es un servicio interno autenticado
if (User.IsInRole("InternalService"))
{
var headerUserId = Request.Headers["X-User-Id"].FirstOrDefault();
if (Guid.TryParse(headerUserId, out var headerGuid))
return headerGuid;
}
return null;
}
CRIT-003: Controllers Sin Atributo [Authorize]¶
Severidad: 🔴 CRÍTICA Archivos: 38 de 39 Controllers
Controllers SIN protección:
| Controller | Endpoints | Datos Expuestos |
|---|---|---|
| AgentsController | 14+ | Configuración de agentes IA |
| ProjectsController | 20+ | Proyectos, secretos, configuración |
| ExecutionsController | 8 | Historial de ejecuciones |
| DevSessionsController | 10+ | Sesiones de desarrollo, mensajes |
| BacklogController | 12+ | Items de backlog |
| OrganizationsController | 12+ | Organizaciones, usuarios |
| CommandsController | 15+ | Comandos del sistema |
| TasksController | 15+ | Tareas programadas |
| CopilotController | 8 | Ejecuciones del copiloto |
| RemoteAgentsController | 10+ | Agentes remotos, API keys |
| ... | ... | ... |
Solución: Agregar [Authorize] a todos los controllers:
// ✅ APLICAR A TODOS LOS CONTROLLERS:
[ApiController]
[Route("api/[controller]")]
[Authorize] // ← AGREGAR
public class ProjectsController : ControllerBase
{
// ...
}
// Para endpoints específicamente públicos:
[AllowAnonymous]
[HttpGet("public/health")]
public IActionResult HealthCheck() => Ok();
CRIT-004: TunnelEndpoints Sin Autenticación¶
Severidad: 🔴 CRÍTICA
Archivo: Orchestrator/src/Orchestrator.Api/Endpoints/TunnelEndpoints.cs
Problema: Endpoints que permiten ejecutar comandos en máquinas remotas están completamente abiertos.
// ❌ CÓDIGO ACTUAL (INSEGURO):
public static void MapTunnelEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/tunnel");
// Generar tokens - SIN AUTH
group.MapPost("/tokens", async (request, tokenService) => ...);
// Listar clientes - SIN AUTH
group.MapGet("/clients", (tunnelService) => ...);
// EJECUTAR COMANDOS REMOTOS - SIN AUTH ❌❌❌
group.MapPost("/clients/{clientId}/execute", async (...) => ...);
// EJECUTAR CLAUDE CLI REMOTO - SIN AUTH ❌❌❌
group.MapPost("/clients/{clientId}/claude", async (...) => ...);
}
Impacto: Cualquier persona puede ejecutar comandos arbitrarios en máquinas remotas conectadas.
Solución:
// ✅ CÓDIGO SEGURO:
public static void MapTunnelEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/tunnel")
.RequireAuthorization("TunnelAdmin"); // ← AGREGAR
// Endpoints protegidos con política específica
group.MapPost("/clients/{clientId}/execute", async (...) => ...)
.RequireAuthorization("TunnelExecute");
group.MapPost("/clients/{clientId}/claude", async (...) => ...)
.RequireAuthorization("TunnelExecute");
}
// En Program.cs:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("TunnelAdmin", policy =>
policy.RequireRole("Admin", "TunnelOperator"));
options.AddPolicy("TunnelExecute", policy =>
policy.RequireRole("Admin")
.RequireClaim("tunnel:execute", "true"));
});
CRIT-005: Webhooks Sin Validación de Firma¶
Severidad: 🔴 CRÍTICA
Archivo: Orchestrator/src/Orchestrator.Api/Controllers/WebhooksController.cs
UltraMsg (WhatsApp) - SIN VALIDACIÓN¶
// ❌ CÓDIGO ACTUAL (INSEGURO):
[HttpPost("ultramsg")]
public async Task<IActionResult> ReceiveUltraMsgWebhook()
{
// NO HAY VALIDACIÓN DE FIRMA
Request.EnableBuffering();
using var reader = new StreamReader(Request.Body, leaveOpen: true);
var body = await reader.ReadToEndAsync();
// Procesa directamente sin validar origen
var payload = JsonSerializer.Deserialize<UltraMsgWebhookPayload>(body);
// ...
}
Sentry/ClickUp - Validación OPCIONAL¶
// ❌ CÓDIGO ACTUAL (INSEGURO):
var secret = _configuration["Sentry:WebhookSecret"];
if (!string.IsNullOrEmpty(secret)) // ← SI NO HAY SECRET, SE ACEPTA TODO
{
if (!ValidateSentrySignature(body, signature, secret))
return Unauthorized();
}
// Continúa sin validación si secret está vacío
Solución:
// ✅ CÓDIGO SEGURO:
[HttpPost("ultramsg")]
public async Task<IActionResult> ReceiveUltraMsgWebhook()
{
var secret = _configuration["UltraMsg:WebhookSecret"]
?? throw new InvalidOperationException("UltraMsg webhook secret not configured");
var signature = Request.Headers["X-UltraMsg-Signature"].FirstOrDefault();
if (string.IsNullOrEmpty(signature))
return Unauthorized(new { error = "Missing signature" });
Request.EnableBuffering();
using var reader = new StreamReader(Request.Body, leaveOpen: true);
var body = await reader.ReadToEndAsync();
Request.Body.Position = 0;
if (!ValidateUltraMsgSignature(body, signature, secret))
return Unauthorized(new { error = "Invalid signature" });
// Procesar webhook validado
}
// Para Sentry/ClickUp - Hacer validación OBLIGATORIA:
[HttpPost("sentry")]
public async Task<IActionResult> ReceiveSentryWebhook()
{
var secret = _configuration["Sentry:WebhookSecret"]
?? throw new InvalidOperationException("Sentry webhook secret MUST be configured");
var signature = Request.Headers["Sentry-Hook-Signature"].FirstOrDefault()
?? return Unauthorized(new { error = "Missing signature" });
// ...validación obligatoria...
}
CRIT-006: Secretos Expuestos en Repositorio¶
Severidad: 🔴 CRÍTICA
Archivos: appsettings.json, appsettings.Development.json
Secretos encontrados en el repositorio:
| Secreto | Ubicación | Tipo |
|---|---|---|
| Claude API Key | appsettings.json | sk-ant-api03-... |
| ClickUp API Token | appsettings.json | pk_93835201_... |
| ClickUp Webhook Secret | appsettings.json | JNJEHSBA5P1M3C7B... |
| Holded API Key | appsettings.json | 9449c7c66349b8d5... |
| SMTP Password | appsettings.json | kyzk brec gubo rsod |
| Encryption Key | appsettings.json | 0221b2e45b9e4e03... |
| PostgreSQL Password | appsettings.json | 6e43b64b5fe04d69... |
| RabbitMQ Password | appsettings.json | admin |
| Tunnel Secret Key | appsettings.json | tunnel-dev-key-... |
Solución Inmediata:
1. Rotar TODOS los secretos expuestos
2. Mover a variables de entorno o Key Vault
3. Agregar appsettings.json a .gitignore (mantener solo appsettings.example.json)
// Program.cs - Usar configuración segura:
builder.Configuration
.AddEnvironmentVariables()
.AddUserSecrets<Program>(optional: true);
// Para producción - Azure Key Vault:
if (!builder.Environment.IsDevelopment())
{
builder.Configuration.AddAzureKeyVault(
new Uri($"https://{vaultName}.vault.azure.net/"),
new DefaultAzureCredential());
}
Análisis por Componente¶
1. Controllers API¶
Estado de Autorización por Controller¶
| Controller | [Authorize] | GetUserId() | Multi-tenant | Rate Limit | Estado |
|---|---|---|---|---|---|
| AgentsController | ❌ | ✅ | Parcial | ❌ | 🔴 |
| ProjectsController | ❌ | ✅ | ✅ | ❌ | 🟡 |
| ExecutionsController | ❌ | ✅ | Parcial | ❌ | 🔴 |
| DevSessionsController | ❌ | ✅ | ✅ | ❌ | 🟡 |
| BacklogController | ❌ | ✅ | ✅ | ❌ | 🟡 |
| OrganizationsController | ❌ | ✅ | ❌ | ❌ | 🔴 |
| TasksController | ❌ | ✅ | Parcial | ❌ | 🔴 |
| CopilotController | ❌ | ❌ | ❌ | ❌ | 🔴 |
| RemoteAgentsController | ❌ | ✅ | ❌ | ❌ | 🔴 |
| CommandsController | ❌ | ❌ | ❌ | ❌ | 🔴 |
| PluginsController | ❌ | ❌ | ❌ | ❌ | 🔴 |
| ProceduresController | ❌ | ✅ | Parcial | ❌ | 🔴 |
| WebhooksController | N/A | N/A | N/A | ❌ | 🟡 |
| ... | ... | ... | ... | ... | ... |
Endpoints Públicos Intencionalmente¶
// Estos endpoints DEBEN ser públicos pero con validación:
[AllowAnonymous] - /webhooks/sentry (con firma HMAC)
[AllowAnonymous] - /webhooks/clickup (con firma HMAC)
[AllowAnonymous] - /webhooks/ultramsg (con firma HMAC)
[AllowAnonymous] - /health (health check)
[AllowAnonymous] - /api/agent-updates/version (versión pública)
2. SignalR Hubs¶
RemoteAgentHub¶
Vulnerabilidades: - API Key hash sin salt (vulnerable a rainbow tables) - Sin rate limiting en Register - Sin validación de permisos en SendCommand
// ❌ CÓDIGO ACTUAL:
private static string HashApiKey(string apiKey)
{
using var sha256 = SHA256.Create();
var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(apiKey));
return Convert.ToBase64String(bytes);
// Sin salt = vulnerable
}
Solución:
// ✅ CÓDIGO SEGURO:
private static string HashApiKey(string apiKey, string salt)
{
using var pbkdf2 = new Rfc2898DeriveBytes(
apiKey,
Encoding.UTF8.GetBytes(salt),
iterations: 100000,
HashAlgorithmName.SHA256);
return Convert.ToBase64String(pbkdf2.GetBytes(32));
}
TunnelHub¶
Vulnerabilidades: - ClientId aceptado sin validación - Sin autenticación de clientes - Permite registro de cualquier máquina
3. OAuth2 (MCP Remote)¶
Vulnerabilidades: - Plain PKCE aceptado (debe ser solo S256) - Sin rate limiting en /oauth2/token - Redirect URI validation incompleta
// ❌ CÓDIGO ACTUAL:
if (method == "S256") { ... }
return codeVerifier == codeChallenge; // Plain PKCE aceptado
// ✅ CÓDIGO SEGURO:
if (method != "S256")
return false; // Rechazar plain PKCE
Matriz de Vulnerabilidades¶
Por Severidad¶
| ID | Vulnerabilidad | Severidad | CVSS | Estado |
|---|---|---|---|---|
| CRIT-001 | Sin autenticación global | CRÍTICO | 9.8 | ⏳ Pendiente |
| CRIT-002 | X-User-Id suplantable | CRÍTICO | 9.1 | ⏳ Pendiente |
| CRIT-003 | Controllers sin [Authorize] | CRÍTICO | 9.1 | ⏳ Pendiente |
| CRIT-004 | TunnelEndpoints sin auth | CRÍTICO | 10.0 | ⏳ Pendiente |
| CRIT-005 | Webhooks sin validación | CRÍTICO | 8.6 | ⏳ Pendiente |
| CRIT-006 | Secretos en repositorio | CRÍTICO | 9.8 | ⏳ Pendiente |
| HIGH-001 | Sin rate limiting | ALTO | 7.5 | ⏳ Pendiente |
| HIGH-002 | Hash API key sin salt | ALTO | 7.4 | ⏳ Pendiente |
| HIGH-003 | IDOR en endpoints | ALTO | 8.1 | ⏳ Pendiente |
| HIGH-004 | Path traversal potencial | ALTO | 7.5 | ⏳ Pendiente |
| MED-001 | Plain PKCE aceptado | MEDIO | 5.9 | ⏳ Pendiente |
| MED-002 | Error details expuestos | MEDIO | 5.3 | ⏳ Pendiente |
| MED-003 | CORS permisivo | MEDIO | 4.3 | ⏳ Pendiente |
| MED-004 | Security headers faltantes | MEDIO | 4.3 | ⏳ Pendiente |
Plan de Securización¶
Fase 1: Crítico (Días 1-3)¶
┌─────────────────────────────────────────────────────────────┐
│ FASE 1: CRÍTICO │
│ Días 1-3 │
├─────────────────────────────────────────────────────────────┤
│ │
│ DÍA 1: │
│ ├── 1. Rotar TODOS los secretos expuestos │
│ ├── 2. Mover secretos a variables de entorno │
│ └── 3. Actualizar .gitignore │
│ │
│ DÍA 2: │
│ ├── 4. Implementar JWT Authentication en Program.cs │
│ ├── 5. Agregar [Authorize] a TODOS los controllers │
│ └── 6. Eliminar confianza en X-User-Id header │
│ │
│ DÍA 3: │
│ ├── 7. Proteger TunnelEndpoints │
│ ├── 8. Hacer validación de webhooks OBLIGATORIA │
│ └── 9. Agregar validación de firma a UltraMsg │
│ │
└─────────────────────────────────────────────────────────────┘
Fase 2: Alto (Días 4-7)¶
┌─────────────────────────────────────────────────────────────┐
│ FASE 2: ALTO │
│ Días 4-7 │
├─────────────────────────────────────────────────────────────┤
│ │
│ DÍA 4-5: │
│ ├── 10. Implementar rate limiting │
│ ├── 11. Agregar security headers middleware │
│ └── 12. Mejorar hash de API keys (PBKDF2) │
│ │
│ DÍA 6-7: │
│ ├── 13. Validar path traversal en endpoints │
│ ├── 14. Completar multi-tenant filtering │
│ └── 15. Implementar IDOR prevention │
│ │
└─────────────────────────────────────────────────────────────┘
Fase 3: Medio (Días 8-14)¶
┌─────────────────────────────────────────────────────────────┐
│ FASE 3: MEDIO │
│ Días 8-14 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ├── 16. Rechazar Plain PKCE (solo S256) │
│ ├── 17. Sanitizar mensajes de error │
│ ├── 18. Refinar políticas CORS │
│ ├── 19. Implementar audit logging │
│ ├── 20. Agregar input validation attributes │
│ └── 21. Security scanning en CI/CD │
│ │
└─────────────────────────────────────────────────────────────┘
Implementación Detallada¶
1. Configuración de JWT Authentication¶
// Program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
// === AUTHENTICATION ===
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)),
ClockSkew = TimeSpan.FromMinutes(5)
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception is SecurityTokenExpiredException)
{
context.Response.Headers.Append("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
// === AUTHORIZATION ===
builder.Services.AddAuthorization(options =>
{
// Política por defecto: requiere autenticación
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
// Políticas específicas
options.AddPolicy("Admin", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("TunnelOperator", policy =>
policy.RequireRole("Admin", "TunnelOperator"));
options.AddPolicy("ApiAccess", policy =>
policy.RequireClaim("api:access", "true"));
});
// === MIDDLEWARE ORDER ===
app.UseAuthentication(); // ANTES de UseAuthorization
app.UseAuthorization();
2. Rate Limiting¶
// Program.cs
using System.Threading.RateLimiting;
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.User.Identity?.Name ?? context.Request.Headers.Host.ToString(),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}));
options.AddPolicy("webhook", context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 50,
Window = TimeSpan.FromMinutes(1)
}));
options.AddPolicy("auth", context =>
RateLimitPartition.GetSlidingWindowLimiter(
partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
factory: _ => new SlidingWindowRateLimiterOptions
{
PermitLimit = 10,
Window = TimeSpan.FromMinutes(1),
SegmentsPerWindow = 4
}));
options.OnRejected = async (context, token) =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.HttpContext.Response.WriteAsJsonAsync(new
{
error = "Too many requests",
retryAfter = context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)
? retryAfter.TotalSeconds
: 60
}, token);
};
});
app.UseRateLimiter();
3. Security Headers Middleware¶
// Middleware/SecurityHeadersMiddleware.cs
public class SecurityHeadersMiddleware
{
private readonly RequestDelegate _next;
public SecurityHeadersMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// Prevent MIME type sniffing
context.Response.Headers.Append("X-Content-Type-Options", "nosniff");
// Prevent clickjacking
context.Response.Headers.Append("X-Frame-Options", "DENY");
// XSS protection
context.Response.Headers.Append("X-XSS-Protection", "1; mode=block");
// Referrer policy
context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
// Content Security Policy
context.Response.Headers.Append("Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self'; " +
"connect-src 'self' wss: https:; " +
"frame-ancestors 'none';");
// HSTS (solo en producción con HTTPS)
if (context.Request.IsHttps)
{
context.Response.Headers.Append("Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload");
}
// Permissions Policy
context.Response.Headers.Append("Permissions-Policy",
"accelerometer=(), camera=(), geolocation=(), gyroscope=(), " +
"magnetometer=(), microphone=(), payment=(), usb=()");
await _next(context);
}
}
// Program.cs
app.UseMiddleware<SecurityHeadersMiddleware>();
4. Input Validation¶
// Validators/ProjectValidator.cs
using FluentValidation;
public class CreateProjectRequestValidator : AbstractValidator<CreateProjectRequest>
{
public CreateProjectRequestValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.MaximumLength(200)
.Matches(@"^[\w\s\-\.]+$")
.WithMessage("Name contains invalid characters");
RuleFor(x => x.Slug)
.NotEmpty()
.MaximumLength(50)
.Matches(@"^[a-z0-9\-]+$")
.WithMessage("Slug must be lowercase alphanumeric with hyphens");
RuleFor(x => x.RepoPath)
.Must(BeValidPath)
.When(x => !string.IsNullOrEmpty(x.RepoPath))
.WithMessage("Invalid repository path");
}
private bool BeValidPath(string? path)
{
if (string.IsNullOrEmpty(path)) return true;
// Prevenir path traversal
if (path.Contains("..") || path.Contains("~"))
return false;
// Validar que es una ruta absoluta válida
try
{
var fullPath = Path.GetFullPath(path);
return fullPath == path;
}
catch
{
return false;
}
}
}
// Program.cs
builder.Services.AddValidatorsFromAssemblyContaining<CreateProjectRequestValidator>();
builder.Services.AddFluentValidationAutoValidation();
5. Audit Logging¶
// Services/AuditService.cs
public interface IAuditService
{
Task LogAsync(AuditEvent auditEvent);
}
public class AuditService : IAuditService
{
private readonly NexusDbContext _db;
private readonly ILogger<AuditService> _logger;
public async Task LogAsync(AuditEvent auditEvent)
{
// Log estructurado
_logger.LogInformation(
"AUDIT: {Action} on {Resource} by {UserId} from {IpAddress}",
auditEvent.Action,
auditEvent.Resource,
auditEvent.UserId,
auditEvent.IpAddress);
// Persistir en base de datos
_db.AuditLogs.Add(new AuditLog
{
Id = Guid.NewGuid(),
Action = auditEvent.Action,
Resource = auditEvent.Resource,
ResourceId = auditEvent.ResourceId,
UserId = auditEvent.UserId,
IpAddress = auditEvent.IpAddress,
UserAgent = auditEvent.UserAgent,
Details = auditEvent.Details,
Timestamp = DateTime.UtcNow
});
await _db.SaveChangesAsync();
}
}
// Uso en controllers:
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(Guid id)
{
await _auditService.LogAsync(new AuditEvent
{
Action = "DELETE",
Resource = "Project",
ResourceId = id.ToString(),
UserId = GetUserId(),
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),
UserAgent = Request.Headers.UserAgent.ToString()
});
// ... delete logic
}
Checklist de Seguridad¶
Pre-Deploy¶
- [ ] Todos los secretos movidos a Key Vault/variables de entorno
- [ ] JWT Authentication configurado
- [ ] [Authorize] en todos los controllers
- [ ] Rate limiting implementado
- [ ] Security headers middleware activo
- [ ] Webhooks con validación obligatoria
- [ ] TunnelEndpoints protegidos
- [ ] X-User-Id header eliminado o restringido
Configuración¶
- [ ] HTTPS forzado en producción
- [ ] CORS restringido a dominios específicos
- [ ] Cookies con Secure y HttpOnly
- [ ] Token expiration < 24 horas
- [ ] Refresh token rotation activo
Monitoreo¶
- [ ] Audit logging implementado
- [ ] Alertas de intentos de autenticación fallidos
- [ ] Monitoreo de rate limiting
- [ ] Logs de acceso a datos sensibles
CI/CD¶
- [ ] Secret scanning habilitado
- [ ] SAST (Semgrep) en pipeline
- [ ] Dependency scanning
- [ ] Security tests automatizados
Próximos Pasos¶
- Inmediato: Rotar secretos y configurar autenticación
- Esta semana: Completar Fase 1 (Crítico)
- Próxima semana: Completar Fase 2 (Alto)
- Mes siguiente: Completar Fase 3 (Medio) + auditoría de penetración
Documento generado como parte de la auditoría de seguridad de Nexus Platform Última actualización: 2026-01-30