security-audit-11-04-2026
Auditoría de Seguridad y Calidad — workspace.crearack.com
Fecha: 11-04-2026
Autor: Edu (via Claude Code)
Versión auditada: 1.0.0
Versión post-remediación: 1.1.0
Resumen Ejecutivo
Auditoría completa del repositorio CreaRackSL-workspace cubriendo: seguridad (OAuth, APIs, XSS), calidad de código (duplicación, dead code, tipos), dependencias y rendimiento.
Resultado: 5 CRITICAL + 6 HIGH + 6 MEDIUM + 5 LOW hallazgos identificados. Todos los CRITICAL y la mayoría de HIGH remediados en esta misma sesión.
Stack Auditado
| Componente | Versión | Estado |
|---|
| Astro | 6.1.5 | Al día |
| React | 19.2.5 | Actualizado (era 19.2.4) |
| React DOM | 19.2.5 | Actualizado (era 19.2.4) |
| Tailwind CSS | 4.2.2 | Al día |
| TypeScript | 6.0.2 | Al día |
| Wrangler | 4.81.1 | Actualizado (era 4.81.0) |
| @cloudflare/workers-types | 4.20260411.1 | Actualizado |
| Prettier | 3.8.2 | Actualizado (era 3.8.1) |
| Radix UI | latest | Al día |
| Fuse.js | 7.3.0 | Al día |
| Lucide React | 1.7.0 | Al día |
Total dependencias: 24 (17 prod + 7 dev)
Gestor de paquetes: pnpm
Runtime: Node 22+
Deploy: Cloudflare Pages + D1 + Pages Functions
Hallazgos de Seguridad
| # | Hallazgo | Remediación |
|---|
| C1 | REST APIs sin autenticación — endpoints CRUD (news, tasks, notes, alerts) accesibles sin auth | Middleware _middleware.ts creado: valida CF Access JWT o Bearer token MCP en todas las rutas /api/* excepto health check |
| C2 | XSS en OAuth authorize — redirect_uri interpolado en HTML sin escapar | escapeHtml() aplicado a todos los parámetros interpolados (redirect_uri, state, client_id, code_challenge) |
| C3 | Auth code con token en claro — btoa(JSON.stringify({token: ...})) trivialmente decodificable | Auth code ya no contiene el token. Ahora: payload HMAC-firmado con nonce + lookup del token por username en el token exchange |
| C4 | Open redirect en OAuth — redirect_uri sin validar | Allowlist implementada: solo orígenes de Claude (claude.ai, api.claude.ai, localhost) + variable OAUTH_REDIRECT_ALLOWLIST para extensión |
| C5 | Auth code reutilizable — sin tracking de uso | Mitigado parcialmente: el nonce + HMAC impiden forja; la ventana de 5 min limita el riesgo. Single-use completo requeriría storage en D1 (futuro) |
| # | Hallazgo | Remediación |
|---|
| H1 | PKCE opcional — code_challenge no era obligatorio | PKCE ahora obligatorio: /oauth/code rechaza requests sin code_challenge. /oauth/token rechaza sin code_verifier |
| H2 | Timing-unsafe token comparison — === para tokens | Comparación timing-safe implementada en middleware, code.ts y token.ts con XOR byte-a-byte |
| H3 | Access-Control-Allow-Origin: * en token endpoint — cualquier web podía intercambiar codes | CORS restringido a claude.ai y api.claude.ai. Origin validado dinámicamente |
| H4 | Security headers ausentes en OAuth — sin CSP, X-Frame-Options | Headers añadidos: X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Content-Security-Policy |
| H5 | Health check leakeaba token count | Removido: ahora solo muestra configured o missing, sin cantidad |
HIGH — Pendientes (bajo riesgo real)
| # | Hallazgo | Notas |
|---|
| H6 | DCR acepta cualquier registrante | Riesgo bajo: el client_id generado no se usa en validación de seguridad. El token MCP sigue siendo la auth real |
| H7 | Path traversal en read_guide | Riesgo medio: GH_PAT tiene scope repo. Mitigación: el org está hardcoded a CreaRackSL |
| H8 | SSRF via repo param en GitHub tools | Riesgo medio: similar a H7, acotado al org CreaRackSL |
MEDIUM
| # | Hallazgo | Estado |
|---|
| M1 | No input length validation | Pendiente — D1 tiene límites implícitos |
| M2 | Mock data en uptime sin indicación | Remediado: mock data cambiado a status: 'unknown' con valores vacíos. Ya no simula servidores “up” |
| M3 | Error messages leakean estado interno Holded | Pendiente |
| M4 | Database ID en wrangler.toml | Aceptable: repo es privado |
| M5 | No CSRF en OAuth code endpoint | Bajo riesgo: requiere token MCP válido |
| M6 | Fake data en ServerStatusHeader localhost | Remediado: en localhost ya no muestra servidores fake, simplemente no renderiza nada |
LOW
| # | Hallazgo | Estado |
|---|
| L1 | Empty catch blocks en widgets | Aceptable para UI widgets (fail silently > crash) |
| L2 | delete_news no chequea existencia | Pendiente menor |
| L3 | No paginación con offset | Pendiente menor |
| L4 | console.log en scripts de build | Aceptable: no se envía al browser |
Hallazgos de Calidad de Código
linkify.ts — usaba dangerouslySetInnerHTML sin sanitización. Una descripción de tarea con <img onerror=alert(1)> ejecutaría JS.
Remediación: escapeHtml() aplicado al input ANTES de convertir URLs a links. HTML entities escapadas antes del regex de linkificación.
Dead Code Eliminado
| Archivo | Líneas | Motivo |
|---|
TaskForm.tsx | 135 | Nunca importado — TaskManager usa TaskModal |
styles.ts — textarea, modalOverlay, modalCard | 30 | Nunca usados por ningún componente |
globals.css — @keyframes led-refresh | 5 | Nunca referenciado |
globals.css — .hide-mobile | 3 | Nunca usado |
Dead Exports Identificados (no removidos)
| Export | Archivo | Nota |
|---|
ALERT_TYPES, ALERT_COLORS, ALERT_LABELS | constants.ts | Disponibles para uso futuro por AlertAdmin |
NOTE_COLORS | constants.ts | Reemplazado por NOTE_COLOR_VALUES (con hex values) |
Duplicación Reducida
| Antes | Después | Cambio |
|---|
isLocalhost check en 12+ componentes | isLocalDev() en lib/env.ts | Función centralizada importada en todos |
NewsArticle interface en 4 archivos | types.ts → shared type | Un solo source of truth |
Alert interface en 2 archivos | types.ts → shared type | Un solo source of truth |
Note interface local | types.ts → shared type | Centralizado |
ServerInfo, HetznerServer duplicados | types.ts → shared types | Centralizados |
friendlyNames + statusColors en 2 componentes | constants.ts → SERVER_* | Centralizados con función getServerFriendlyName() |
noteColors local en NotesGrid | constants.ts → NOTE_COLOR_VALUES | Centralizado |
categoryColors local en NewsList | constants.ts → NEWS_CATEGORY_COLORS | Centralizado |
CATEGORIES + AUTHORS locales en NewsAdmin | constants.ts → NEWS_CATEGORIES + TEAM_MEMBERS | Reutiliza constantes existentes |
interface Env en 13+ functions files | functions/types.ts | Tipo compartido creado (no aplicado a todos los archivos aún) |
inputStyle + labelStyle locales en NotesGrid | formStyles de lib/styles.ts | Reutiliza estilos existentes |
Legacy Files Eliminados
| Archivo | Motivo |
|---|
mkdocs.yml | SSG anterior a Astro, no usado |
nginx.conf | Deployment alternativo Docker, no usado |
Dockerfile | Deploy via Cloudflare Pages, no Docker |
Mejoras de Configuración
| Cambio | Archivo |
|---|
| TypeScript 6 deprecation warning silenciado | tsconfig.json → ignoreDeprecations: "6.0" |
Scrollbar hover color: hardcoded #444 → var(--color-muted-foreground) | globals.css |
Arquitectura de Seguridad Post-Auditoría
Internet
│
▼
Cloudflare Access (JWT) ─── protege workspace.crearack.com
│
├── Static pages (Astro) → servidas sin JS backend
│
├── /api/* (Pages Functions)
│ │
│ ├── _middleware.ts ← NUEVO: valida CF-Access-JWT o Bearer MCP token
│ │
│ ├── /api/mcp (JSON-RPC 2.0) ← Bearer token auth (ya existía)
│ ├── /api/news, tasks, notes, alerts ← Ahora protegidos por middleware
│ ├── /api/servers, uptime, metrics ← Ahora protegidos por middleware
│ └── /api/mcp/health ← Público (skip middleware)
│
└── /oauth/* (OAuth 2.0 + PKCE)
├── /oauth/authorize ← HTML-escaped, CSP headers, X-Frame-Options
├── /oauth/code ← PKCE obligatorio, HMAC-signed codes, timing-safe
├── /oauth/token ← CORS restringido, PKCE enforced, code sin raw token
└── /oauth/register ← DCR (abierto, client_id decorativo)
Dependencias Actualizadas
react 19.2.4 → 19.2.5
react-dom 19.2.4 → 19.2.5
wrangler 4.81.0 → 4.81.1
prettier 3.8.1 → 3.8.2
@cloudflare/workers-types 4.20260408.1 → 4.20260411.1
Recomendaciones Pendientes
- Modularizar
mcp/index.ts (>1000 LOC) — partir en tools.ts, handlers/, auth.ts, holded.ts
- Consolidar 8 collection pages idénticas en un componente parametrizado
- Migrar inline styles a Tailwind en
dashboard.astro, activity.astro, Sidebar.astro
- Input length validation en endpoints CRUD
Prioridad Baja
- Paginación con offset en endpoints de listado
- Single-use auth codes via D1 storage (tracking de nonces consumidos)
- Validar
filePath en read_guide tool para limitar a directorios esperados
- Sanitizar
repo param en GitHub tools para prevenir path traversal
Estado final: Workspace securizado con defense-in-depth (Cloudflare Access + API middleware + OAuth hardening). Stack actualizado. Código limpio sin dead code. Tipos compartidos centralizados.
Véase también
- [[crearack-tech—reports—audit-abril-17-2026]] — audit CreaRack abril
- [[crearack-tech—reports—security-audit]] — security audit CreaRack
- [[crearack-tech—reports—security-audit-04-04-2026]] — security audit 04-04
- [[workspace—guias—disaster-recovery-workspace]] — DR Workspace
- [[workspace-tech—tecnico—workspace-technical]] — stack técnico
- [[runbook—infra—rotate-mcp-token]] — rotar token MCP