diff --git a/FLUJOS/BACK_BACK/FLUJOS_APP.js b/FLUJOS/BACK_BACK/FLUJOS_APP.js index ef880b47..4522bd30 100755 --- a/FLUJOS/BACK_BACK/FLUJOS_APP.js +++ b/FLUJOS/BACK_BACK/FLUJOS_APP.js @@ -11,28 +11,46 @@ const { MongoClient } = require('mongodb'); const app = express(); const port = process.env.PORT || 3000; -// Configurar Helmet con CSP personalizado +// Helmet completo: X-Powered-By off, XSS protection, nosniff, frameguard, HSTS, etc. +app.use(helmet()); + +// CSP personalizado encima del helmet por defecto app.use( helmet.contentSecurityPolicy({ directives: { - defaultSrc: ["'self'"], - scriptSrc: [ - "'self'", - "'unsafe-inline'", - 'https://unpkg.com', - 'https://cdnjs.cloudflare.com', - 'https://fonts.googleapis.com', - 'https://fonts.gstatic.com', - ], - styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'], - imgSrc: ["'self'", 'data:', 'blob:', 'https://unpkg.com'], - connectSrc: ["'self'", 'ws://localhost:3000'], - fontSrc: ["'self'", 'https://fonts.gstatic.com'], + defaultSrc: ["'self'"], + scriptSrc: ["'self'", 'https://unpkg.com', 'https://cdnjs.cloudflare.com'], + styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'], + imgSrc: ["'self'", 'data:', 'blob:'], + connectSrc: ["'self'"], + fontSrc: ["'self'", 'https://fonts.gstatic.com'], + frameAncestors: ["'none'"], + objectSrc: ["'none'"], + baseUri: ["'self'"], }, }) ); -app.use(bodyParser.json()); +// Prevenir CSRF: solo aceptar peticiones GET a /api desde el mismo origen +app.use('/api/', (req, res, next) => { + const origin = req.headers.origin; + const referer = req.headers.referer; + // En producción detrás de nginx no hay Origin en peticiones same-site normales, + // pero sí lo habría en peticiones cross-origin maliciosas + if (origin && origin !== `https://theflows.net` && origin !== `http://localhost:${port}`) { + return res.status(403).json({ error: 'Origen no permitido' }); + } + next(); +}); + +// Añadir cabecera anti-clickjacking y anti-MIME-sniffing explícitas +app.use((req, res, next) => { + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); + next(); +}); + +app.use(bodyParser.json({ limit: '10kb' })); // **Definir primero las rutas de la API** @@ -53,13 +71,25 @@ app.get('/api/data', async (req, res) => { const imagenesWikiCollection = db.collection('imagenes_wiki'); // scrapeadas (fallback) const comparacionesCollection = db.collection('comparaciones'); - // Sanitizar parámetros: rechazar cualquier valor que no sea string plano - // (previene MongoDB operator injection como tema[$ne]=x) + // ── Sanitización y validación de parámetros ────────────────────────────── + // Rechaza cualquier valor que no sea string (bloquea operator injection) function sanitizeParam(val) { if (typeof val !== 'string') return undefined; return val.trim(); } - const tema = sanitizeParam(req.query.tema); + + // Lista blanca de temas válidos (evita escaneo libre de la BD) + const TEMAS_VALIDOS = new Set([ + 'guerra global', 'inteligencia y seguridad', 'cambio climático', + 'demografía y sociedad', 'economía y corporaciones', 'otros', + 'geopolítica conflictos', 'seguridad internacional espionaje', + 'libertad de prensa periodismo', 'corporaciones poder económico', + 'populismo extremismo', 'desinformación redes sociales', + 'privacidad vigilancia masiva', 'biodiversidad medioambiente', + 'inteligencia artificial tecnología', + ]); + + const temaRaw = sanitizeParam(req.query.tema); const subtematica = sanitizeParam(req.query.subtematica); const palabraClave = sanitizeParam(req.query.palabraClave); const fechaInicio = sanitizeParam(req.query.fechaInicio); @@ -67,38 +97,59 @@ app.get('/api/data', async (req, res) => { const nodos = sanitizeParam(req.query.nodos); const complejidad = sanitizeParam(req.query.complejidad); - console.log('Parámetros de consulta:', req.query); - - // Validar que el parámetro 'tema' esté presente + // Validar tema contra lista blanca + const tema = temaRaw && TEMAS_VALIDOS.has(temaRaw) ? temaRaw : null; if (!tema) { - res.status(400).json({ error: 'El parámetro "tema" es obligatorio' }); + res.status(400).json({ error: 'El parámetro "tema" es obligatorio o no válido' }); return; } - // Construir la consulta para obtener los nodos (noticias, wikipedia y torrents) - let nodesQuery = { - tema: tema, - }; - if (subtematica) nodesQuery.subtema = subtematica; + // Validar subtematica: solo letras, números, espacios y guiones (máx 80 chars) + if (subtematica && !/^[\w\s\-áéíóúñüÁÉÍÓÚÑÜ]{1,80}$/.test(subtematica)) { + res.status(400).json({ error: 'subtematica no válida' }); + return; + } + + // Validar palabraClave: máx 100 chars, escapar metacaracteres regex + let regexKw = null; if (palabraClave) { if (palabraClave.length > 100) { res.status(400).json({ error: 'palabraClave demasiado larga' }); return; } - const escapedKw = palabraClave.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - nodesQuery.texto = { $regex: escapedKw, $options: 'i' }; + regexKw = palabraClave.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } + + // Validar fechas: formato YYYY-MM-DD estricto + const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/; + let fechaGte = null, fechaLte = null; if (fechaInicio && fechaFin) { - nodesQuery.fecha = { - $gte: new Date(fechaInicio), - $lte: new Date(fechaFin), - }; + if (!ISO_DATE.test(fechaInicio) || !ISO_DATE.test(fechaFin)) { + res.status(400).json({ error: 'Formato de fecha inválido (YYYY-MM-DD)' }); + return; + } + fechaGte = new Date(fechaInicio); + fechaLte = new Date(fechaFin); + if (isNaN(fechaGte) || isNaN(fechaLte) || fechaGte > fechaLte) { + res.status(400).json({ error: 'Rango de fechas inválido' }); + return; + } } - console.log('Consulta para nodos:', nodesQuery); - // Obtener el límite de nodos (con un máximo para evitar sobrecarga) - const nodosLimit = Math.min(parseInt(nodos) || 100, 500); + // Validar nodos: entero positivo entre 1 y 500 + const nodosRaw = parseInt(nodos, 10); + const nodosLimit = (!isNaN(nodosRaw) && nodosRaw > 0) ? Math.min(nodosRaw, 500) : 100; + // Validar complejidad: float entre 0 y 100 + const complejidadRaw = parseFloat(complejidad); + const porcentajeSimilitudMin = (!isNaN(complejidadRaw) && complejidadRaw >= 0 && complejidadRaw <= 100) + ? complejidadRaw : 0; + + // Construir query de MongoDB solo con valores validados + const nodesQuery = { tema }; + if (subtematica) nodesQuery.subtema = subtematica; + if (regexKw) nodesQuery.texto = { $regex: regexKw, $options: 'i' }; + if (fechaGte) nodesQuery.fecha = { $gte: fechaGte, $lte: fechaLte }; // Ejecutar la consulta y obtener los resultados de las colecciones console.log('Ejecutando consulta para nodos en colecciones...'); // Límite de imágenes: un tercio del total para no saturar el grafo @@ -156,10 +207,6 @@ app.get('/api/data', async (req, res) => { const nodeIds = formattedNodes_final.map(node => node.id); console.log('IDs de nodos:', nodeIds); - // Leer el parámetro 'complejidad' de la query (que en tu front envías como % de similitud) - const porcentajeSimilitudMin = parseFloat(complejidad) || 0; - console.log('Porcentaje de similitud mínimo (desde complejidad):', porcentajeSimilitudMin); - // Construir la consulta para obtener los enlaces relacionados con los nodos obtenidos const linksQuery = { porcentaje_similitud: { $gte: porcentajeSimilitudMin }, diff --git a/FLUJOS/VISUALIZACION/public/3dscript_eco-corp.html b/FLUJOS/VISUALIZACION/public/3dscript_eco-corp.html index 976505a7..62d840f4 100755 --- a/FLUJOS/VISUALIZACION/public/3dscript_eco-corp.html +++ b/FLUJOS/VISUALIZACION/public/3dscript_eco-corp.html @@ -85,24 +85,27 @@ cubeContainer.style.top = position.y + 'px'; cubeContainer.style.left = position.x + 'px'; - cubeContainer.innerHTML = ` -
-
País ${i}
-
Detalle ${i}
-
Año ${2000 + i}
-
Muertos ${(i + 1) * 1000}
-
ONU
-
Fuente
-
- `; + const cube = document.createElement('div'); + cube.className = 'cube'; + [['front', `País ${i}`], ['back', `Detalle ${i}`], + ['left', `Año ${2000 + i}`], ['right', `Muertos ${(i + 1) * 1000}`], + ['top', 'ONU'], ['bottom', 'Fuente']].forEach(([cls, txt]) => { + const face = document.createElement('div'); + face.className = `face ${cls}`; + face.textContent = txt; + cube.appendChild(face); + }); + cubeContainer.appendChild(cube); // Crear un popup para cada cubo const popup = document.createElement('div'); popup.classList.add('popup'); - popup.innerHTML = ` -

Noticia relacionada con País ${i}

- - `; + const p = document.createElement('p'); + p.textContent = `Noticia relacionada con País ${i}`; + const btn = document.createElement('button'); + btn.textContent = 'Close'; + popup.appendChild(p); + popup.appendChild(btn); popup.querySelector('button').addEventListener('click', () => { popup.style.display = 'none'; diff --git a/FLUJOS/VISUALIZACION/public/script_eco-corp.html b/FLUJOS/VISUALIZACION/public/script_eco-corp.html index 16595626..24fccca9 100755 --- a/FLUJOS/VISUALIZACION/public/script_eco-corp.html +++ b/FLUJOS/VISUALIZACION/public/script_eco-corp.html @@ -38,16 +38,17 @@ cubeContainer.style.top = position.y + 'px'; cubeContainer.style.left = position.x + 'px'; - cubeContainer.innerHTML = ` -
-
País ${i+1}
-
Detalle ${i+1}
-
Año ${2010 + i}
-
Muertos ${(i+1)*1000}
-
ONU
-
Fuente
-
- `; + const cube = document.createElement('div'); + cube.className = 'cube'; + [['front', `País ${i+1}`], ['back', `Detalle ${i+1}`], + ['left', `Año ${2010 + i}`], ['right', `Muertos ${(i+1)*1000}`], + ['top', 'ONU'], ['bottom', 'Fuente']].forEach(([cls, txt]) => { + const face = document.createElement('div'); + face.className = `face ${cls}`; + face.textContent = txt; + cube.appendChild(face); + }); + cubeContainer.appendChild(cube); cubePositions.push({x: position.x + 35, y: position.y + 35}); // Ajustar al centro del cubo graphContainer.appendChild(cubeContainer);