| # Analisis de bugs y refactorizacion |
|
|
| Fecha de revision: 2026-06-08 |
|
|
| ## Resumen ejecutivo |
|
|
| El proyecto compila y la app importa correctamente, pero hay fallos funcionales y de seguridad que pueden hacer que la interfaz prometa capacidades que el backend no ejecuta, que algunas busquedas fallen silenciosamente, y que datos externos se rendericen como HTML sin escape. |
|
|
| Hallazgos mas importantes: |
|
|
| - La configuracion de fuentes esta desalineada: la UI usa IDs en mayusculas y el motor usa IDs en minusculas. |
| - Scopus, CORE y SerpAPI no reciben API keys aunque existan en `.env`. |
| - La normalizacion de anos puede romper busquedas con `ValueError` o `TypeError`. |
| - Hay varias rutas de XSS/HTML injection al insertar metadatos externos en `gr.HTML`. |
| - Los controles de sistema pueden matar todos los procesos `python.exe`. |
| - La app crea y muestra credenciales por defecto `admin/admin123`. |
| - Las API keys se devuelven al frontend en la pestana de modelos. |
| - El mecanismo de detener pipeline usa `StopAsyncIteration` de forma insegura para async generators. |
| - La refactorizacion prioritaria debe centralizar fuentes/providers, sanitizar salidas HTML y separar configuracion sensible del cliente. |
|
|
| ## Verificaciones realizadas |
|
|
| - `python -m compileall -q .`: OK. |
| - `venv\Scripts\python.exe -c "import app; print('app import ok')"`: OK. |
| - `python -m pytest -q`: falla porque `pytest` no esta instalado. |
| - `venv\Scripts\python.exe -m pytest -q`: falla porque `pytest` no esta instalado en el `venv`. |
| - Se reprodujeron fallos con providers simulados: |
| - filtro de fuentes con `OPENALEX` devuelve 0 resultados, con `openalex` devuelve 1. |
| - `year='s.f.'` rompe el filtro de anos. |
| - mezcla `year='2020'` y `year=2019` rompe el ordenamiento. |
|
|
| ## Severidad |
|
|
| - P0: riesgo de seguridad o accion destructiva. |
| - P1: rompe flujo principal o fuente importante. |
| - P2: inconsistencia funcional, deuda tecnica con alto costo futuro. |
| - P3: mejora de robustez, UX o mantenibilidad. |
|
|
| ## Hallazgos detallados |
|
|
| ### 1. Configuracion de fuentes inconsistente |
|
|
| Severidad: P1 |
|
|
| Archivos: |
|
|
| - `modules/config/sources_config_tab.py` |
| - `backend/tools/search_engine.py` |
| - `backend/providers/sources.py` |
| - `config.py` |
| - `modules/research_tab.py` |
|
|
| Sintoma: |
|
|
| La pestana de configuracion guarda fuentes como `OPENALEX`, `SCOPUS`, `LA_REFERENCIA`, pero el motor de busqueda filtra contra `openalex`, `scopus`, `lareferencia`. |
|
|
| Causa raiz: |
|
|
| Hay varias fuentes de verdad: |
|
|
| - `modules/config/sources_config_tab.py` define IDs en mayusculas. |
| - `backend/providers/sources.py` define grupos reales usados por `search_engine`. |
| - `config.py` define otro mapa mas amplio con fuentes que no estan implementadas. |
| - `modules/research_tab.py` define `ALL_SOURCES` manualmente. |
|
|
| Ademas, en `sources_config_tab.py`, la linea que inicializa `_enabled_sources` dentro de `create_sources_config_tab()` no declara `global`, por lo que crea una variable local. Al arrancar, la configuracion queda ignorada. Cuando el usuario cambia un checkbox, `_update_enabled()` si usa `global`, pero guarda IDs en mayusculas y puede filtrar todo. |
|
|
| Impacto: |
|
|
| - La UI puede decir que una fuente esta activa, pero el backend no la usa. |
| - El usuario puede desactivar/activar fuentes y dejar la busqueda sin providers efectivos. |
| - Dificulta diagnosticar por que "no hay resultados". |
|
|
| Correccion recomendada: |
|
|
| - Crear un registro unico de fuentes, por ejemplo `backend/providers/registry.py`. |
| - Usar siempre IDs canonicos en minusculas. |
| - Hacer que la UI derive sus opciones desde ese registro. |
| - Normalizar aliases antes de guardar configuracion. |
| - Eliminar o consolidar `config.py` si no es la fuente real. |
|
|
| Criterio de aceptacion: |
|
|
| - Activar `OpenAlex` en UI guarda `openalex`. |
| - Desactivar `openalex` realmente evita llamadas a OpenAlex. |
| - Seleccionar `all` expande solo providers implementados o marca claramente los no implementados. |
|
|
| Pruebas minimas: |
|
|
| - `expand_sources(["all"])` solo devuelve IDs canonicos. |
| - `enabled_sources=["OPENALEX"]` se normaliza a `["openalex"]`. |
| - `enabled_sources=["openalex"]` permite resultados de OpenAlex. |
|
|
| ### 2. Scopus, CORE y SerpAPI no reciben API keys |
|
|
| Severidad: P1 |
|
|
| Archivos: |
|
|
| - `backend/tools/search_engine.py` |
| - `backend/providers/scopus.py` |
| - `backend/providers/core_.py` |
| - `backend/providers/serpapi.py` |
|
|
| Sintoma: |
|
|
| Los providers premium o con clave devuelven lista vacia si `api_key` no se pasa como argumento. El motor los llama asi: |
|
|
| ```python |
| PROVIDERS[src](query, limit=min(max_results, 50)) |
| ``` |
|
|
| Causa raiz: |
|
|
| Las claves estan en `.env`, pero no existe una capa que lea `SCOPUS_API_KEY`, `CORE_API_KEY` o `SERPAPI_API_KEY` y las inyecte al provider correspondiente. |
|
|
| Impacto: |
|
|
| - Scopus, CORE y SerpAPI aparecen configurables pero no funcionan. |
| - El sistema no distingue "sin resultados" de "no habia credencial". |
|
|
| Correccion recomendada: |
|
|
| - En el registro unico, cada provider debe declarar `env_key`, `requires_key` y `callable`. |
| - `search_engine.search()` debe resolver la clave por provider y pasarla como `api_key`. |
| - Si falta una clave requerida, devolver metadata tipo `sourceStatus` en lugar de silencio. |
|
|
| Criterio de aceptacion: |
|
|
| - Con `SCOPUS_API_KEY` en `.env`, `search(..., sources=["scopus"])` llama a Scopus con la clave. |
| - Sin clave, el resultado indica `scopus: missing_api_key`. |
|
|
| ### 3. Filtro y ordenamiento por ano rompen con datos heterogeneos |
|
|
| Severidad: P1 |
|
|
| Archivo: |
|
|
| - `backend/tools/search_engine.py` |
|
|
| Sintoma: |
|
|
| El filtro usa `int(r.get("year", 0))` sin validar. Si un provider devuelve `s.f.`, `N/A`, una fecha completa o un string no numerico, la busqueda completa falla. |
|
|
| Reproduccion: |
|
|
| - `year='s.f.'` + `year_start='2020'` lanza `ValueError`. |
| - `year='2020'` y `year=2019` sin filtro lanza `TypeError` al ordenar. |
|
|
| Causa raiz: |
|
|
| No hay normalizacion de metadatos a la entrada del motor. |
|
|
| Correccion recomendada: |
|
|
| - Crear `parse_year(value) -> Optional[int]`. |
| - Normalizar todos los resultados inmediatamente despues de recibirlos. |
| - Ordenar con una key que siempre devuelva int: `parse_year(x.get("year")) or 0`. |
| - Los filtros deben ignorar o conservar documentos sin ano segun politica explicita. |
|
|
| Criterio de aceptacion: |
|
|
| - `year="2020"`, `year=2020`, `year="2020-05-01"` se tratan como 2020. |
| - `year="s.f."`, `None`, `"N/A"` no rompen. |
|
|
| ### 4. HTML injection / XSS en resultados, referencias y grafo |
|
|
| Severidad: P0 |
|
|
| Archivos: |
|
|
| - `modules/search_tab.py` |
| - `modules/research_tab.py` |
| - `modules/graph_module.py` |
| - `assets/custom.js` |
|
|
| Sintoma: |
|
|
| Campos de proveedores externos se insertan directo en HTML: |
|
|
| - titulo |
| - autores |
| - abstract |
| - DOI |
| - PDF URL |
| - fuente |
| - contenido generado por IA |
|
|
| Tambien se usan `onclick` inline y `innerHTML`. |
|
|
| Impacto: |
|
|
| Un resultado academico malicioso o un dato corrupto puede inyectar HTML/JS en la app. Aunque sea local, el riesgo aumenta si se comparte con usuarios, se usa `share=True`, o se abre en red. |
|
|
| Correccion recomendada: |
|
|
| - Escapar texto con `html.escape`. |
| - Validar URLs con `urllib.parse`; permitir solo `http` y `https`. |
| - Construir atributos JS con `json.dumps`, no con reemplazos manuales. |
| - Evitar `onclick` inline; delegar eventos desde JS con `data-*`. |
| - En grafo, usar `textContent` para texto y crear nodos DOM en vez de concatenar strings con `innerHTML`. |
| - Revisar salida Markdown a HTML; si se usa `markdown`, sanitizar o restringir tags. |
|
|
| Criterio de aceptacion: |
|
|
| - Un titulo como `<img src=x onerror=alert(1)>` se muestra como texto, no ejecuta codigo. |
| - Una URL `javascript:alert(1)` no se renderiza como link. |
|
|
| ### 5. Controles destructivos: reiniciar y matar procesos |
|
|
| Severidad: P0 |
|
|
| Archivo: |
|
|
| - `app.py` |
|
|
| Sintoma: |
|
|
| Los botones de control ejecutan: |
|
|
| - `taskkill /F /IM python.exe /T` |
| - `taskkill` con `shell=True` |
|
|
| Impacto: |
|
|
| - Puede matar la app actual. |
| - Puede matar otros procesos Python del usuario. |
| - Puede interrumpir trabajos no relacionados. |
|
|
| Correccion recomendada: |
|
|
| - Eliminar el boton "Matar Procesos" de la UI normal. |
| - Si se requiere restart, reiniciar solo el proceso actual con un supervisor controlado. |
| - Evitar `shell=True`. |
| - No matar por nombre de proceso global. |
|
|
| Criterio de aceptacion: |
|
|
| - Ningun boton de UI mata todos los `python.exe`. |
| - Reinicio, si existe, afecta solo a la instancia actual. |
|
|
| ### 6. Credenciales por defecto y hash debil |
|
|
| Severidad: P0/P1 segun despliegue |
|
|
| Archivo: |
|
|
| - `app.py` |
|
|
| Sintoma: |
|
|
| La app crea `admin/admin123` y lo muestra en el mensaje de login. La contrasena se almacena con SHA-256 simple, sin sal ni factor de coste. |
|
|
| Impacto: |
|
|
| - Cualquier usuario que vea el login sabe la credencial. |
| - Si se filtra la base, el hash es barato de romper. |
|
|
| Correccion recomendada: |
|
|
| - Exigir `LETXIPU_ADMIN_PASSWORD` o crear usuario en un comando setup. |
| - No mostrar credenciales en UI. |
| - Usar `passlib` con bcrypt/argon2 o `werkzeug.security`. |
| - Forzar cambio de password inicial. |
|
|
| Criterio de aceptacion: |
|
|
| - Sin password configurado, la app no crea admin debil. |
| - El mensaje de login no contiene credenciales. |
|
|
| ### 7. API keys expuestas al frontend |
|
|
| Severidad: P0/P1 |
|
|
| Archivo: |
|
|
| - `modules/config/ai_tab.py` |
|
|
| Sintoma: |
|
|
| La pestana de modelos precarga `MISTRAL_API_KEY` en un textbox y al cambiar provider devuelve la clave al cliente. |
|
|
| Impacto: |
|
|
| - Cualquier persona autenticada puede inspeccionar el HTML/estado y extraer secretos. |
| - Aumenta el riesgo si la app se comparte. |
|
|
| Correccion recomendada: |
|
|
| - Mostrar solo estado: configurada/no configurada. |
| - Si se permite actualizar claves, hacerlo con un flujo de escritura al `.env` cuidadosamente autorizado. |
| - Nunca devolver claves existentes al navegador. |
|
|
| Criterio de aceptacion: |
|
|
| - El frontend no recibe valores completos de API keys. |
| - El endpoint/evento solo devuelve mascara, por ejemplo `sk-...abcd`. |
|
|
| ### 8. Detener pipeline puede terminar como error generico |
|
|
| Severidad: P1/P2 |
|
|
| Archivos: |
|
|
| - `backend/pipeline.py` |
| - `modules/research_tab.py` |
|
|
| Sintoma: |
|
|
| `_checkpoint()` levanta `StopAsyncIteration`. En async generators, Python convierte esto en `RuntimeError: async generator raised StopAsyncIteration`. |
|
|
| Impacto: |
|
|
| - El boton detener puede mostrar error generico en vez de estado "detenido". |
| - La limpieza final puede no ejecutarse como se esperaba. |
|
|
| Correccion recomendada: |
|
|
| - Crear excepcion propia: |
|
|
| ```python |
| class PipelineStopped(Exception): |
| pass |
| ``` |
|
|
| - Levantar `PipelineStopped`. |
| - Capturar `PipelineStopped` en handlers. |
|
|
| Criterio de aceptacion: |
|
|
| - Pulsar detener muestra estado detenido y no un traceback/error generico. |
|
|
| ### 9. Cambio de proveedor IA en Research devuelve forma incorrecta |
|
|
| Severidad: P2 |
|
|
| Archivo: |
|
|
| - `modules/research_tab.py` |
|
|
| Sintoma: |
|
|
| `update_models()` devuelve un solo `gr.update`, pero esta conectado a tres outputs: busqueda, sintesis y traduccion. |
|
|
| Impacto: |
|
|
| - Al cambiar proveedor, Gradio puede fallar o actualizar solo un componente. |
|
|
| Correccion recomendada: |
|
|
| - Devolver tres updates: |
|
|
| ```python |
| update = gr.update(choices=models, value=models[0]) |
| return update, update, update |
| ``` |
|
|
| o una lista de tres updates. |
|
|
| Criterio de aceptacion: |
|
|
| - Cambiar de `mistral` a `groq` actualiza los tres dropdowns. |
|
|
| ### 10. Descarga de PDFs: SSRF, TLS deshabilitado y sin limite de tamano |
|
|
| Severidad: P0/P1 |
|
|
| Archivos: |
|
|
| - `backend/tools/pdf_tools.py` |
| - `backend/tools/pdf_processor.py` |
| - `modules/pdf_tab.py` |
| - `modules/chat_tab.py` |
|
|
| Sintoma: |
|
|
| Se descargan URLs ingresadas por el usuario desde el servidor. Algunas descargas usan `verify=False`. No hay limite de tamano ni allowlist/bloqueo de IPs internas. |
|
|
| Impacto: |
|
|
| - SSRF contra servicios internos si la app se expone. |
| - Descarga de archivos enormes que agotan memoria o disco. |
| - TLS sin verificar permite MITM. |
|
|
| Correccion recomendada: |
|
|
| - Activar verificacion TLS. |
| - Bloquear IPs privadas/locales: `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, link-local, metadata cloud. |
| - Limitar tamano por `Content-Length` y streaming con max bytes. |
| - Permitir solo `http`/`https`. |
| - Reusar un downloader comun. |
|
|
| Criterio de aceptacion: |
|
|
| - URL `http://127.0.0.1/...` se rechaza. |
| - PDF mayor al limite se corta con error claro. |
|
|
| ### 11. Catalogo de fuentes promete providers no implementados |
|
|
| Severidad: P2 |
|
|
| Archivos: |
|
|
| - `README.md` |
| - `config.py` |
| - `modules/config/sources_config_tab.py` |
| - `backend/providers/sources.py` |
| - `backend/tools/search_engine.py` |
|
|
| Sintoma: |
|
|
| Se mencionan fuentes como SciELO, CONAHCyT, UNAM, ANID, OasisBR, SNRD, MinCiencias, OpenReview, PapersWithCode o HuggingFace, pero el mapa real `PROVIDERS` no contiene implementaciones para muchas de ellas. |
|
|
| Impacto: |
|
|
| - La UI genera expectativas falsas. |
| - Los grupos `all`, `latam`, `ai_ml` pueden incluir fuentes que no hacen nada. |
|
|
| Correccion recomendada: |
|
|
| - El registro unico debe marcar `implemented=True/False`. |
| - La UI debe ocultar fuentes no implementadas o mostrarlas como "proximamente". |
| - Los grupos operativos deben incluir solo implementadas. |
|
|
| Criterio de aceptacion: |
|
|
| - No se puede seleccionar una fuente no implementada como si estuviera activa. |
|
|
| ### 12. Manejo de errores silencioso en providers |
|
|
| Severidad: P2 |
|
|
| Archivos: |
|
|
| - `backend/providers/*.py` |
| - `backend/providers/base.py` |
| - `backend/tools/search_engine.py` |
|
|
| Sintoma: |
|
|
| Muchos providers hacen `except Exception: return []`. `fetch_json()` tambien convierte cualquier error en `{"error": str(e)}`. |
|
|
| Impacto: |
|
|
| - Timeouts, credenciales faltantes, 403/429 y errores de parseo se ven igual que "0 resultados". |
| - Dificulta depurar fuentes rotas. |
|
|
| Correccion recomendada: |
|
|
| - Devolver estructura por fuente: |
|
|
| ```python |
| { |
| "source": "openalex", |
| "ok": true, |
| "results": [], |
| "error": None, |
| "status": "ok" |
| } |
| ``` |
|
|
| - `search()` debe conservar `sourceErrors` y mostrarlos en UI. |
|
|
| Criterio de aceptacion: |
|
|
| - Si PubMed falla por timeout, la UI muestra "PubMed timeout" y no solo "sin resultados". |
|
|
| ## Plan de refactorizacion recomendado |
|
|
| ### Fase 1: estabilizacion funcional |
|
|
| Objetivo: que las busquedas basicas sean confiables. |
|
|
| Tareas: |
|
|
| - Crear `backend/providers/registry.py`. |
| - Consolidar grupos y aliases en un solo lugar. |
| - Normalizar IDs de fuentes a minusculas. |
| - Pasar API keys por provider desde `.env`. |
| - Agregar `parse_year()` y normalizacion de resultados. |
| - Corregir `update_models()` para multiples outputs. |
| - Reemplazar `StopAsyncIteration` por `PipelineStopped`. |
|
|
| Resultado esperado: |
|
|
| - `search()` no se cae por anos raros. |
| - Fuentes activadas en UI coinciden con providers usados. |
| - Scopus/CORE/SerpAPI usan claves si existen. |
| - Detener pipeline funciona sin error generico. |
|
|
| ### Fase 2: seguridad de interfaz y secretos |
|
|
| Objetivo: eliminar riesgos P0. |
|
|
| Tareas: |
|
|
| - Escapar HTML en `search_tab`, `research_tab` y `graph_module`. |
| - Validar links antes de renderizar. |
| - Quitar `onclick` inline donde sea posible. |
| - No devolver API keys al frontend. |
| - Remover credenciales por defecto o exigir password por env. |
| - Eliminar controles `taskkill` globales. |
|
|
| Resultado esperado: |
|
|
| - Datos externos no ejecutan HTML/JS. |
| - Las claves no viajan al navegador. |
| - La app no mata procesos ajenos. |
|
|
| ### Fase 3: downloader PDF seguro |
|
|
| Objetivo: robustecer lectura/vectorizacion/chat con PDFs. |
|
|
| Tareas: |
|
|
| - Crear `backend/tools/downloader.py`. |
| - Bloquear IPs internas y esquemas no permitidos. |
| - Descargar en streaming con limite de tamano. |
| - Activar TLS verification. |
| - Unificar `pdf_tools.py`, `pdf_processor.py` y `chat_tab.py`. |
|
|
| Resultado esperado: |
|
|
| - El PDF local funciona sin abrir SSRF ni OOM. |
|
|
| ### Fase 4: arquitectura de resultados y errores |
|
|
| Objetivo: dejar de mezclar "sin resultados" con "fuente rota". |
|
|
| Tareas: |
|
|
| - Definir `SearchResult` y `SourceSearchOutcome`. |
| - Hacer que providers devuelvan resultados normalizados. |
| - Agregar `sourcesUsed`, `sourcesSkipped`, `sourceErrors`. |
| - Mostrar estado por fuente en UI. |
|
|
| Resultado esperado: |
|
|
| - La UI puede decir: "OpenAlex OK, PubMed timeout, Scopus falta API key". |
|
|
| ### Fase 5: pruebas y CI local |
|
|
| Objetivo: evitar regresiones. |
|
|
| Tareas: |
|
|
| - Agregar `pytest` a `requirements-dev.txt` o `requirements.txt`. |
| - Tests unitarios para: |
| - expansion de fuentes |
| - normalizacion de aliases |
| - parseo de anos |
| - providers con/sin API key |
| - escape HTML |
| - detener pipeline |
| - Tests de integracion con providers simulados. |
|
|
| Resultado esperado: |
|
|
| - `pytest -q` corre sin depender de red. |
| - Los bugs reproducidos quedan cubiertos. |
|
|
| ## Orden de implementacion sugerido |
|
|
| 1. `registry.py` de fuentes/providers. |
| 2. Normalizacion de resultados y anos. |
| 3. API keys por provider. |
| 4. Correccion de `update_models()` y `PipelineStopped`. |
| 5. Escape HTML y validacion de URLs. |
| 6. Retiro de `taskkill`, credenciales por defecto y exposicion de secrets. |
| 7. Downloader PDF seguro. |
| 8. Tests. |
|
|
| ## Archivos mas importantes para tocar |
|
|
| - `backend/tools/search_engine.py` |
| - `backend/providers/sources.py` |
| - `backend/providers/registry.py` nuevo |
| - `modules/config/sources_config_tab.py` |
| - `modules/research_tab.py` |
| - `modules/search_tab.py` |
| - `modules/graph_module.py` |
| - `modules/config/ai_tab.py` |
| - `backend/tools/pdf_tools.py` |
| - `backend/tools/pdf_processor.py` |
| - `modules/chat_tab.py` |
| - `app.py` |
| - `requirements.txt` o `requirements-dev.txt` |
|
|
| ## Nota sobre el estado del repo |
|
|
| El arbol de trabajo ya tenia cambios modificados y archivos sin seguimiento antes de este informe. No se debe hacer `reset` ni revertir esos cambios sin revisar su origen. |
|
|