Pickle sobre gRPC: RCE sin autenticar en un servidor de políticas robóticas
LeRobot, de Hugging Face, transmitía su canal de inferencia robot-a-política mediante pickle sobre gRPC sin autenticar: cualquier host que alcanzara el puerto obtenía ejecución remota de código. El parche de junio de 2026 elimina pickle.
¿Qué es esto?
El 29 de junio de 2026, el equipo de investigación de SecureLayer7 publicó el análisis de una falla de ejecución remota de código sin autenticar en LeRobot, el framework de robótica de código abierto de Hugging Face. El pipeline de inferencia asíncrona de LeRobot divide una política robótica en un par cliente–servidor: un cliente que corre en el robot físico transmite observaciones a un PolicyServer alojado en un host GPU independiente, que devuelve las acciones. El transporte es gRPC, y las cargas útiles viajaban como bytes pickle en crudo.
La falla es la consecuencia previsible de esa decisión. Este análisis se basa en el aviso público y el parche del proyecto; aquí no se reproduce ningún exploit funcional. La lección es de arquitectura de despliegue más que de modelo: cuando un componente de servicio deserializa datos de red no confiables con pickle, y el canal carece de autenticación, la red es la vulnerabilidad.
Cómo funciona
El pickle de Python no es un formato de datos: es un pequeño bytecode de máquina de pila cuyos opcodes invocan invocables arbitrarios durante la carga. CPython lo dice sin rodeos: «El módulo pickle no es seguro. Solo deserialice datos de confianza.» Cualquier objeto puede definir qué ocurre al deserializarse, de modo que un blob manipulado puede ejecutar un comando en cuanto se deserializa, antes de cualquier validación posterior.
En la versión afectada, los manejadores de inferencia asíncrona leían directamente del cable los bytes accesibles al atacante y los pasaban a pickle.loads():
# patrón vulnerable (ilustrativo)
policy_specs = pickle.loads(request.data) # nosec <-- se ejecuta antes de la validación
Se combinaban dos problemas. Primero, la deserialización ocurría antes del control de tipo destinado a validar el mensaje, así que el objeto ya estaba construido, y es en la construcción donde se ejecuta el trabajo peligroso. Segundo, el listener gRPC se enlazaba con add_insecure_port(...): sin TLS, sin token, sin autenticación mutua. La topología por defecto supone que el robot y el servidor de políticas comparten una red de confianza (una VPC o una malla Tailscale), pero en cuanto el puerto (TCP/50051 por defecto) queda accesible para un actor no confiable, esa suposición se derrumba. La falla es además bidireccional: el servidor también serializaba sus respuestas de acción, de modo que un servidor de políticas malicioso o comprometido podía volver la misma primitiva contra cada cliente robótico conectado.
Un indicio recurrente en el código: un comentario # nosec junto a cada llamada pickle, una anotación que silencia la advertencia de análisis estático (la regla B301 de Bandit) sin abordar el riesgo de ejecución que señala.
Por qué importa
El código afectado se distribuía en el framework usado para entrenar y servir políticas de robots reales, por lo que la población expuesta incluye clústeres GPU de investigación compartidos, topologías de entrenamiento multimáquina, notebooks de demostración dejados en marcha y despliegues en la nube donde el operador nunca advirtió que gRPC estaba a la escucha. Un solo puerto alcanzable concede ejecución de código bajo el proceso del servidor —reportado como root dentro del contenedor de referencia—, lo que en un host GPU coloca a un paso los artefactos de modelos, los conjuntos de datos y las credenciales de la nube.
La ironía es punzante: Hugging Face escribió safetensors precisamente para eliminar «el problema de pickle» en los archivos de modelos, y su documentación desaconseja en negrita cargar archivos pickle de fuentes no confiables. El mismo antipatrón reaparece en otro rincón de la pila —el transporte de inferencia—, es decir, exactamente donde los fallos de deserialización siguen escondiéndose en el ecosistema de servicio de IA.
Defensas
Nunca llame a pickle.loads() sobre datos controlados por la red. La corrección duradera es un formato de cable tipado y no ejecutable. El parche del proyecto reemplazó pickle por safetensors para los tensores y JSON para los metadatos, envueltos en una estructura que porta una etiqueta de tipo que el deserializador valida contra el tipo esperado. Eso elimina el intérprete de opcodes, impone un esquema y rechaza un mensaje enrutado al manejador equivocado.
Autentique el transporte. gRPC no implica autenticación: el mTLS o la autenticación por token son opcionales. Enlace los puntos de servicio con credenciales, no con add_insecure_port, y exija TLS mutuo entre robot y servidor de políticas.
Trate la red como hostil. Denegación por defecto del tráfico entrante hacia los puertos de inferencia, escucha en loopback o una interfaz privada, y nunca exponga un puerto de política/inferencia a una red que un tercero no confiable pueda alcanzar. Segmente los hosts GPU lejos de las cargas de trabajo generales.
Cace el patrón de forma estática. Busque en los servicios Python expuestos a la red los pickle.loads sobre cualquier dato que remonte a una entrada de solicitud, y trate un # nosec contiguo como un hallazgo, no como una resolución.
Mínimo privilegio y control de salida. Restrinja el rol en la nube del proceso de servicio al mínimo, y alerte si un proceso servidor de políticas genera un hijo inesperado (un shell, un intérprete) o establece conexiones salientes que jamás debería hacer.
Estado
| Elemento | Detalle |
|---|---|
| Referencia | CVE-2026-25874 (véase la ficha de cve.org) |
| Divulgación | Aviso publicado el 29 de junio de 2026 |
| Afectado | Inferencia asíncrona de LeRobot (PolicyServer / cliente robótico) en v0.4.3 y anteriores |
| Causa raíz | pickle.loads() sobre entrada gRPC sin autenticar, deserializada antes de la validación de tipo |
| Impacto | Ejecución remota de código previa a la autenticación en el servidor de políticas; bidireccional (un servidor malicioso puede alcanzar clientes) |
| Corrección | PR del proyecto #3048 — pickle eliminado, sustituido por un esquema tipado safetensors + JSON; actualice y habilite autenticación de transporte / aislamiento de red |