RCE sin autenticación en la sandbox del OpenHands Software Agent SDK (CVE-2026-112346)
El OpenHands Software Agent SDK expone el puerto del agente en todas las interfaces, sin autenticación por defecto: RCE como root. 1.52.0 corrige Docker, no Apptainer.
Resumen ejecutivo (TL;DR)
- OpenHands Software Agent SDK ejecuta un agente en un espacio de trabajo. Su servidor de agente es el servicio HTTP y WebSocket que ejecuta comandos y operaciones sobre archivos; un lanzador de espacios de trabajo, DockerWorkspace o ApptainerWorkspace, inicia ese servicio. El lanzador Docker del SDK OpenHands Software Agent publica el puerto del servidor de agente de la sandbox en todas las interfaces del host, y su lanzador Apptainer vincula el servidor a
0.0.0.0directamente en el host. Una lista vacía de claves de API de sesión desactiva la autenticación por defecto. Cualquier host que pueda alcanzar el puerto puede enviar solicitudes HTTP o WebSocket sin autenticación que ejecutan comandos de shell arbitrarios como root dentro de la sandbox. Esto incluye un contenedor en una red Docker compartida, un equipo de la red local o un cliente en Internet cuando el host está expuesto. - Todas las versiones de openhands-sdk anteriores a 1.52.0 están afectadas. En el momento de escribir este artículo, el paquete registraba 604,050 descargas en PyPI durante la última semana.
- VulnCheck, que asignó y publicó la CVE, la califica como crítica, con CVSS v3.1 9.8 y CVSS v4.0 9.3, CWE-1327 (vinculación a una dirección IP sin restricciones), y acredita a mil4n. No se conoce explotación alguna y no existe ninguna prueba de concepto pública.
- Para Docker, actualice a openhands-sdk 1.52.0 o posterior, publicada el 5 de octubre de 2026, vuelva a crear cada contenedor de sandbox y confirme que el puerto se publica ahora solo en 127.0.0.1. En despliegues más antiguos, bloquee el acceso de red al puerto.
- La versión 1.52.0 no cambia nada para Apptainer, que sigue vinculándose a
0.0.0.0: definaSESSION_API_KEY(dejeOH_SESSION_API_KEYS_0sin definir, o asigne el mismo valor a ambas) y mantenga el puerto fuera de redes no fiables. En Docker anterior a 1.52.0, definir una clave protege el servidor pero rompe la sesión del SDK; actualice para recuperar el uso autenticado.
¿Estoy afectado?
Está afectado si usa openhands-sdk con el lanzador DockerWorkspace en una versión anterior a 1.52.0 o con el lanzador ApptainerWorkspace, no hay ninguna clave de API de sesión definida y el puerto de la sandbox es accesible desde una red en la que no confía plenamente. El rango afectado del aviso termina antes de 1.52.0, pero el lanzador Apptainer sigue vinculándose a 0.0.0.0 en 1.52.0. Deben cumplirse las tres condiciones:
- Su código o su plataforma utiliza el SDK OpenHands Software Agent, paquete
openhands-sdk, y lo controla con el lanzador DockerWorkspace en una versión anterior a 1.52.0, o con el lanzador ApptainerWorkspace, que la versión 1.52.0 no modifica. - No hay ninguna clave de API de sesión configurada, que es la situación por defecto: ni
SESSION_API_KEYniOH_SESSION_API_KEYS_0están definidas en el entorno desde el que se lanza el SDK, por lo que la listasession_api_keysdel servidor está vacía y la autenticación está desactivada. - El puerto publicado es accesible para alguien en quien no confía plenamente. El lanzador anterior a la corrección lo publica en todas las interfaces del host, lo que incluye otros contenedores de una red bridge de Docker compartida, cualquier equipo de la red local del host e Internet si el host tiene una dirección pública y ningún firewall que lo bloquee.
| Paquete | Versiones afectadas | Versión corregida |
|---|---|---|
| openhands-sdk (PyPI) | anteriores a 1.52.0 | 1.52.0, publicada el 5 de octubre de 2026 |
| Lanzador DockerWorkspace del SDK OpenHands Software Agent | anteriores a 1.52.0 | 1.52.0: publica en 127.0.0.1, y su cliente lee la clave de sesión configurada |
| Lanzador ApptainerWorkspace del SDK OpenHands Software Agent | anteriores a 1.52.0 según el aviso, y también 1.52.0 | 1.52.0 solo cambia la publicación de Docker; Apptainer sigue vinculándose a 0.0.0.0 en v1.52.0. Configure una clave de sesión y aísle el puerto (consulte la sección sobre la corrección). |
Compruebe qué ejecuta y dónde quedó publicado el puerto:
python -c "from importlib.metadata import version; print(version('openhands-sdk'))" 2>/dev/null || echo 'openhands-sdk not installed in this interpreter'
docker ps --format '{{.Names}} {{.Ports}}' | grep -E 'agent-server|->8000/tcp'
# Run where the SDK starts; report presence without printing secret values.
python - <<'PYKEYS'
import os
for name in ("SESSION_API_KEY", "OH_SESSION_API_KEYS_0"):
print(name + ": " + ("set" if os.environ.get(name) else "unset or empty"))
PYKEYS
En la salida de docker ps, una sandbox anterior a la corrección muestra una línea de publicación como 0.0.0.0:31245->8000/tcp, es decir, todas las interfaces, mientras que el lanzador corregido muestra 127.0.0.1:31245->8000/tcp. El comando Docker run del lanzador nombra cada contenedor agent-server-<uuid>.
¿Cómo se corrige?
DockerWorkspace: actualice el SDK y luego vuelva a crear cada contenedor de sandbox, ya que el contenedor en ejecución conserva su antigua asignación de puertos hasta que se vuelve a crear:
python -m pip install --upgrade 'openhands-sdk>=1.52.0'
A continuación, finalice cada sesión de sandbox en ejecución e inicie una nueva, para que el lanzador actualizado cree un contenedor nuevo con la nueva asignación de puertos (un simple docker restart conserva la asignación antigua), y confirme la nueva dirección de publicación con docker ps. El cliente del lanzador Docker corregido lee OH_SESSION_API_KEYS_0 del entorno del proceso del SDK y, si no existe, recurre a SESSION_API_KEY. Esto coincide con la selección de clave del servidor, por lo que las solicitudes del SDK llevan la clave configurada.
ApptainerWorkspace: la versión 1.52.0 no cambia la vinculación a 0.0.0.0 del lanzador, por lo que actualizar no basta para cerrar la exposición del puerto. Configure la clave de sesión y aísle el puerto como se describe a continuación.
Si todavía no puede actualizar, o si utiliza Apptainer, restrinja el acceso de red y configure la autenticación. Los límites propios de cada lanzador son los siguientes:
- Bloquee el puerto: una regla del firewall del host o una regla de la cadena
DOCKER-USERde Docker que descarte el tráfico no local hacia los puertos de la sandbox cierra la ruta remota, pero el puerto sigue vinculado en todas las interfaces, y cualquier proceso o contenedor que todavía pueda alcanzarlo dispone de la API completa sin autenticación. - Configure la autenticación: ejecute
export SESSION_API_KEY="$(openssl rand -hex 32)"en el entorno desde el que se inicia el proceso del SDK, antes de que se inicie; el lanzador reenvía la clave y el servidor la exige. Una sesión de sandbox que ya está en ejecución conserva su servidor sin clave, así que inicie una nueva. - Apptainer: su cliente lee
SESSION_API_KEY, también en v1.52.0. Defina esa variable y dejeOH_SESSION_API_KEYS_0sin definir, o asigne el mismo valor a ambas, para que el cliente y el servidor coincidan. - Docker anterior a 1.52.0: el cliente fija su clave de forma rígida en
None. Definir cualquiera de las dos variables de clave protege el servidor pero rompe la sesión del SDK; utilícelo como contención temporal y actualice para recuperar la sesión.
¿Cuál es la causa raíz?
El lanzador publicaba el puerto de la sandbox en todas las interfaces del host y no configuraba ninguna clave de sesión, mientras anulaba de forma explícita el valor por defecto seguro del propio servidor de agente, que se vincula a loopback cuando no hay ninguna clave definida.
El código vulnerable es openhands-workspace/openhands/workspace/docker/workspace.py en el SDK en la versión 1.51.0. El lanzador construye el comando Docker run que inicia la sandbox y su servidor de agente:
ports = ["-p", f"{self.host_port}:8000"] # line 218
if self.extra_ports:
ports += [
"-p",
f"{self.host_port + 1}:8001", # VSCode # line 222
]
La opción -p de Docker publica un puerto del contenedor en el host. Sin una IP de host en la especificación, como en este caso, la dirección de publicación es 0.0.0.0: todas las interfaces que tenga el host. Con extra_ports activado, que por defecto está desactivado, el lanzador también publicaba el puerto 8001 de VS Code en host_port + 1 sin IP de host, por lo que ese segundo puerto también quedaba en todas las interfaces. Más adelante en el mismo comando run, el lanzador pasa de forma explícita al servidor de agente su dirección de escucha:
run_cmd = [
"docker", "run", "-d", "--platform", self.platform, "--rm",
...
image,
"--host",
"0.0.0.0", # line 248-250
"--port",
"8000",
]
Dentro del contenedor, vincularse a 0.0.0.0 es lo que necesita la redirección de puertos. La exposición depende de dónde queda la publicación en el lado del host, y son todas las interfaces. La autenticación es la otra mitad. Una vez iniciado el contenedor, el lanzador configura el propio cliente del SDK para el workspace:
if not self.host:
object.__setattr__(self, "host", f"http://127.0.0.1:{self.host_port}")
object.__setattr__(self, "api_key", None) # line 272
Clave del cliente: la clave del cliente Docker está fijada de forma rígida en None. Por otro lado, el lanzador reenvía al contenedor las variables de entorno de clave de sesión que existan, y solo el servidor las lee; el propio cliente del SDK nunca toma la clave.
Configuración reenviada: la descripción del campo forward_env del propio lanzador Docker explica que estas variables permiten al servidor autenticar las solicitudes cuando escucha en 0.0.0.0.
Valor por defecto del servidor: sin una variable de clave configurada, la lista session_api_keys del servidor está vacía. El registro de la CVE identifica este valor por defecto como el que desactiva la autenticación.
El valor por defecto seguro del servidor de agente se introdujo en julio de 2026: el pull request #4180, «fix(agent-server): default bind host to loopback without a session API key», hizo que un servidor sin clave se vinculara a loopback. El --host 0.0.0.0 explícito del lanzador anula ese valor por defecto, y por eso la corrección tuvo que cambiar el lanzador y no el servidor.
¿Cómo se explota?
Cualquier cliente que alcance el puerto puede ejecutar comandos de shell como root en la sandbox con simples solicitudes a la API sin autenticar. No hay código de exploit público. Para comprobar la exposición sin payload, ejecute curl -s -o /dev/null -w '%{http_code}\n' http://<sandbox-host>:<port>/api/conversations/count desde la posición de red que quiera verificar: 200 significa que la API responde sin clave, 401 que se exige una clave de sesión y 000 que el puerto no es accesible desde allí. La ruta de ataque que se desprende del registro de la CVE y del código fuente requiere cuatro pasos:
- El atacante puede alcanzar el host en el puerto publicado: por ejemplo, un contenedor en la misma red bridge de Docker que el host de la sandbox, un equipo de la red local o cualquier host de Internet si la máquina está expuesta y sin filtrado.
- El atacante envía una solicitud HTTP o WebSocket a la API del servidor de agente. No se requiere ninguna clave, porque el servidor se inició con una lista
session_api_keysvacía. - El servidor ejecuta el comando de shell solicitado como root dentro del contenedor de la sandbox y devuelve todo lo que el comando imprime.
- Además de la ejecución de comandos, la misma API da acceso al directorio del workspace montado y al almacén de secretos, y el registro de la CVE indica que los secretos se leen en texto plano.
Nada en la cadena requiere una vulnerabilidad en la propia solicitud: la API es la del SDK, diseñada para que la use el agente, y simplemente está escuchando a cualquiera.
¿Cómo se desarrolla el ataque?
Una solicitud sin autenticación a un puerto publicado por error se convierte en ejecución de comandos como root dentro de la propia sandbox del agente, con el workspace y el almacén de secretos adjuntos.
red local, contenedor, Internet"] --> B["Puerto del agente accesible desde la red
Docker: -p sin IP de host
Apptainer: --host 0.0.0.0"] B --> C["Servidor de agente con la lista
session_api_keys vacía"] C --> D["Solicitud HTTP o WebSocket
sin autenticación aceptada"] D --> E["Comando de shell ejecutado
como root en la sandbox"] E --> F["Lectura de los archivos del workspace
y del almacén de secretos en texto plano"] style B fill:#f8d7da style C fill:#f8d7da
¿Qué puede hacer un atacante?
El radio de impacto es el contenedor de la sandbox y los datos montados en él. Según el registro de la CVE, un atacante puede ejecutar comandos de shell arbitrarios como root dentro del contenedor de la sandbox, acceder al workspace montado sobre el que trabaja el agente y leer en texto plano los secretos del almacén de secretos que el SDK aprovisiona para la sesión. Es el conjunto de trabajo del agente: el repositorio o directorio que edita, las credenciales de su almacén de secretos y cualquier otra cosa que la sesión haya montado. Dos condiciones lo hacen posible:
- Accesibilidad de red al puerto publicado, que el lanzador anterior a la corrección abre en todas las interfaces; sin autenticación, sin interacción del usuario y sin ningún privilegio más allá de una posición en la red.
- Un despliegue que nunca configuró una clave de sesión, que es el valor por defecto del SDK.
¿Por qué funciona la corrección?
La corrección es el commit 7b66584, fusionado como el pull request #5209 («fix(workspace): publish Docker ports on loopback») el 3 de octubre de 2026. Se publicó en v1.52.0 el 5 de octubre y aborda tanto la dirección de publicación de Docker como la búsqueda de clave del cliente:
- ports = ["-p", f"{self.host_port}:8000"]
+ ports = ["-p", f"127.0.0.1:{self.host_port}:8000"]
if self.extra_ports:
ports += [
"-p",
- f"{self.host_port + 1}:8001", # VSCode
+ f"127.0.0.1:{self.host_port + 1}:8001", # VSCode
- object.__setattr__(self, "api_key", None)
+ session_api_key = os.environ.get(
+ "OH_SESSION_API_KEYS_0", os.environ.get("SESSION_API_KEY")
+ )
+ object.__setattr__(self, "api_key", session_api_key)
La publicación ahora se hace solo en loopback, de modo que ningún otro host de la red puede alcanzar el puerto, y el puerto de VS Code sigue la misma regla. Ese es el compromiso: un cliente en otra máquina ya no puede conectarse directamente a los puertos publicados. La pull request presenta el cambio como una forma de mantener el comportamiento de red por defecto local a la máquina, ya que el propio cliente del SDK ya se conecta a través de 127.0.0.1. El servidor dentro del contenedor sigue vinculándose a 0.0.0.0, lo que verifica la propia prueba de regresión de la corrección: la vinculación interna del contenedor es lo que necesita la redirección de Docker, y la dirección de publicación en el lado del host es la frontera.
El cliente ahora resuelve la misma clave que acepta el servidor, dando preferencia a la variable actual OH_SESSION_API_KEYS_0 y usando la antigua SESSION_API_KEY como alternativa, de modo que una clave configurada autentica en ambos sentidos y no solo en las comprobaciones de entrada del servidor.
La corrección cambia el lanzador Docker, y sus pruebas cubren la publicación en loopback y la búsqueda de clave del cliente. El aviso de seguridad también menciona Apptainer, pero su lanzador sigue pasando --host 0.0.0.0 en v1.52.0.
Para Apptainer, la corrección no cambia nada: el lanzador ejecuta el servidor de agente directamente en el host con --host 0.0.0.0, en 1.52.0 igual que antes, por lo que el puerto escucha en todas las interfaces. La contención es la de la sección sobre la corrección: una clave de sesión y aislamiento de red. El servidor escucha en el host_port del espacio de trabajo, un puerto libre entre 30000 y 39999 si no fija uno. Ejecute ss -ltn | grep ':<port> ' en el host para ver si escucha en 0.0.0.0 y, después, nc -vz <host> <port> desde una máquina que no debería alcanzarlo: si la conexión se establece, el puerto está expuesto y solo lo protegen el firewall y la clave.
¿Cómo compruebo si he sido comprometido?
No se han publicado indicadores de compromiso para este fallo y no se conoce ninguna explotación, por lo que no hay una lista de IOC que buscar. Esto es lo que puede comprobar en un despliegue que ejecutó una sandbox anterior a 1.52.0 con un puerto accesible:
- La asignación de puertos (
docker ps --format '{{.Names}} {{.Ports}}'): una sandbox anterior a la corrección muestra0.0.0.0:en la línea de publicación, lo que confirma que el puerto se publicaba en todas las interfaces. Es la mitad de la condición de exposición: sus reglas de firewall y su ruta de red determinan si algo no fiable podía alcanzarlo. - Los registros del contenedor de la sandbox, en busca de solicitudes HTTP o WebSocket que no correspondan a la actividad de sus sesiones de agente, ya que el servidor de agente registra las solicitudes que atiende.
- El contenido de su almacén de secretos, para todo lo que la sandbox pudiera alcanzar: considere divulgado cada secreto aprovisionado para una sandbox expuesta, rótelo y reconstruya el contenedor a partir de una imagen limpia.
Si la condición de exposición nunca se cumplió, porque el puerto estaba bloqueado por un firewall o el host no tenía ninguna ruta de red no fiable, el riesgo práctico era prácticamente nulo y basta con aplicar el parche según el calendario habitual.
Cronología
- 23 de julio de 2026: el servidor de agente adquiere su valor por defecto seguro, la vinculación a loopback cuando no hay ninguna clave de API de sesión definida, en el pull request #4180.
- 3 de octubre de 2026: el pull request #5209, «fix(workspace): publish Docker ports on loopback», se fusiona como el commit 7b66584.
- 5 de octubre de 2026: se publica v1.52.0; su registro de cambios menciona la corrección por su título sin presentarla como un asunto de seguridad, y no se publica ningún aviso de seguridad.
- 26 de octubre de 2026: VulnCheck publica CVE-2026-112346, CVSS v3.1 9.8 y v4.0 9.3, CWE-1327, acreditando a mil4n, con el rango afectado anterior a 1.52.0.
- 26 de octubre de 2026: la base de datos de avisos de seguridad de GitHub sincroniza el registro como GHSA-rp7h-crm7-h5qj.
- 27 de octubre de 2026: este artículo.
Preguntas frecuentes (FAQ)
¿La plataforma principal de OpenHands comparte el fallo?
El aviso de seguridad cubre los lanzadores DockerWorkspace y ApptainerWorkspace del SDK, que utilizan las sesiones locales del SDK. El aviso no establece si la plataforma alojada de OpenHands u otro producto integra estos lanzadores, por lo que los operadores de cualquier despliegue basado en OpenHands deberían realizar las mismas comprobaciones: la versión instalada del SDK y la dirección de publicación de cualquier contenedor agent-server. El patrón que hay que buscar es un contenedor llamado agent-server-* que publique el puerto 8000 en 0.0.0.0.
¿Por qué se publicó v1.52.0 sin un aviso de seguridad?
El registro de cambios de v1.52.0 menciona el pull request por su título, «fix(workspace): publish Docker ports on loopback», sin presentarlo como un asunto de seguridad, y ningún aviso acompañó a la versión. Por eso los escáneres de dependencias no tuvieron nada sobre lo que alertar hasta que apareció el registro de la CVE de VulnCheck el 26 de octubre, y por eso las comprobaciones de versión, y no solo las fuentes de CVE, fueron la señal fiable en ese intervalo.
¿Basta con una clave de sesión como protección?
Cierra la ruta de ejecución de comandos sin autenticación: el servidor comprueba la clave de sesión en las rutas protegidas de conversaciones, ejecución de comandos y acceso a archivos. Las rutas públicas de información del servidor no son una prueba fiable de la autenticación. No cambia el puerto: un lanzador Docker anterior a 1.52.0 sigue publicando en todas las interfaces y su propio cliente no puede enviar la clave, así que la sesión se rompe. Un lanzador Apptainer anterior a 1.52.0 sigue funcionando con SESSION_API_KEY definida, ya que su cliente la lee. Para Docker, la actualización añade la publicación en loopback. Para Apptainer, mantenga el puerto aislado de la red además de autenticado; 1.52.0 no cambia su dirección de vinculación.
¿Tengo que volver a crear los contenedores en ejecución?
Sí. La asignación de puertos se establece al crear el contenedor, por lo que una sandbox que ya está en ejecución conserva su publicación en 0.0.0.0 incluso después de actualizar el SDK en el host. Detenga y vuelva a crear cada sandbox y luego confirme la nueva asignación con docker ps.
¿Por qué no hubo ningún aviso durante las tres semanas posteriores a la corrección?
El registro de la CVE, la entrada GHSA y las correspondencias de las bases de datos datan todos del 26 de octubre, tres semanas después de la versión corregida. Los despliegues que actualizaron con rapidez estaban protegidos sin saber por qué, y los que fijan versiones no tuvieron ningún aviso sobre el que alertar mientras tanto. Este es el argumento habitual a favor de una buena higiene de versiones en la infraestructura de agentes: conviene seguir los componentes de sandbox que se vinculan a puertos de red a nivel de versión, y no solo a nivel de CVE.
¿Dónde sigue openhands-sdk por debajo de 1.52.0?
Ejecute Ostorlab SCA sobre sus repositorios e imágenes de contenedor para listar cada proyecto y cada versión publicada que todavía incluya openhands-sdk anterior a 1.52.0.
Referencias
- Registro de CVE-2026-112346, publicado el 26 de octubre de 2026 por VulnCheck como CNA, con los vectores CVSS v3.1 9.8 y v4.0 9.3, CWE-1327, el rango afectado anterior a 1.52.0 y la descripción de ambos lanzadores: https://www.cve.org/CVERecord?id=CVE-2026-112346 (consultado el 28 de octubre de 2026)
- GHSA-rp7h-crm7-h5qj, la entrada de la base de datos de avisos de seguridad de GitHub sincronizada a partir del registro de la CVE: https://github.com/advisories/GHSA-rp7h-crm7-h5qj (consultado el 27 de octubre de 2026)
- Aviso de VulnCheck, «OpenHands Software Agent SDK < 1.52.0 Unauthenticated Sandbox Agent Server RCE», severidad crítica, crédito a mil4n: https://www.vulncheck.com/advisories/openhands-software-agent-sdk-unauthenticated-sandbox-agent-server-rce (consultado el 27 de octubre de 2026)
- Pull request de la corrección #5209, «fix(workspace): publish Docker ports on loopback», fusionado el 3 de octubre de 2026, con la justificación de la propagación de la clave al cliente en su descripción: https://github.com/OpenHands/software-agent-sdk/pull/5209 (consultado el 27 de octubre de 2026)
- Commit de la corrección 7b66584ff8899144eb4ce679c2811861342af9de, el diff de la publicación en loopback y de la resolución de la clave de sesión: https://github.com/OpenHands/software-agent-sdk/commit/7b66584ff8899144eb4ce679c2811861342af9de (consultado el 27 de octubre de 2026)
- Versión v1.52.0, publicada el 5 de octubre de 2026, que menciona la corrección sin presentarla como un asunto de seguridad: https://github.com/OpenHands/software-agent-sdk/releases/tag/v1.52.0 (consultado el 27 de octubre de 2026)
- Código fuente vulnerable en v1.51.0, openhands-workspace/openhands/workspace/docker/workspace.py, líneas 89 a 95 (
forward_env), 218, 222, 248 a 250 y 272: https://github.com/OpenHands/software-agent-sdk/blob/v1.51.0/openhands-workspace/openhands/workspace/docker/workspace.py (consultado el 27 de octubre de 2026) - Valor por defecto seguro del servidor de agente, pull request #4180, «fix(agent-server): default bind host to loopback without a session API key», fusionado el 23 de julio de 2026: https://github.com/OpenHands/software-agent-sdk/pull/4180 (consultado el 27 de octubre de 2026)
- Estadísticas de descargas de PyPI para openhands-sdk, 604,050 descargas en la semana consultada: https://pypistats.org/packages/openhands-sdk (consultado el 27 de octubre de 2026)
- Lanzador Apptainer en v1.52.0, que mantiene la vinculación a
0.0.0.0y la búsqueda deSESSION_API_KEYpor parte del cliente: https://github.com/OpenHands/software-agent-sdk/blob/v1.52.0/openhands-workspace/openhands/workspace/apptainer/workspace.py (consultado el 28 de octubre de 2026)