Todos los artículos
IA

Ataque LiteLLM: cómo un repositorio limpio acabó instalando un ladrón de credenciales

Ataque LiteLLM reconstruido: cómo los wheels 1.107.0 y las etiquetas de Docker trajeron un ladrón de claves, cronología en hora de Pekín y cómo limpiarlo.

Un paquete cerrado sobre una cinta transportadora azul con un sobre sin etiqueta escondido bajo la cinta adhesiva: el ataque LiteLLM, código limpio y versión envenenada
También disponible en中文English

En algún momento entre las 19:40 y las 21:12, más o menos, hora de Pekín del 21 de octubre de 2026 (11:40–13:12 UTC), mucha gente escribió pip install -U litellm y recibió la versión 1.107.0. Nada parecía raro. La etiqueta de GitHub estaba limpia, las notas de la versión eran normales y el paquete se importaba sin errores. Pero los siete wheels traían un ladrón de credenciales que se ejecuta cada vez que arranca Python, además de un cargador pensado para descargar y ejecutar más código después. Este ataque LiteLLM merece entenderse bien, porque se saltó la única comprobación que casi todos hacemos: «he mirado el código y está bien».

He pasado la noche leyendo el issue oficial #47826 con todos sus comentarios, el análisis de AISLE, los de JFrog y Aikido, los registros de PyPI y Docker, y los propios commits del atacante. Aquí va lo que pasó, en orden y con fuentes. Cuando algo todavía no se sabe, lo digo. Si solo quieres arreglarlo, salta a la autocomprobación. La versión corta también está en nuestra guía de errores de Claude Code.

Lo esencial

  • Qué salió envenenado: litellm==1.107.0 en PyPI (los 7 wheels) y las imágenes Docker oficiales v1.107.0, latest, main-latest y main-stable en Docker Hub y GHCR. Fuente: issue #47826.
  • Qué hace: deja cook.pth, cook.py y sitecustomize.py en site-packages. El ladrón se ejecuta en cada arranque de Python, aunque nada importe litellm, y manda un paquete cifrado a www.litellm.dev. El dominio real del proyecto es litellm.ai.
  • Estado a las 23:18, hora de Pekín: PyPI muestra la 1.107.0 en cuarentena y la última versión es la 1.106.2. A las 22:32 los mantenedores dijeron que habían eliminado las imágenes Docker maliciosas. Las etiquetas ya no están, pero en nuestra prueba Docker Hub seguía sirviendo la imagen envenenada por digest (más abajo lo explico).
  • Lo que aún no se sabe: cómo entró el atacante en el despliegue interno de LiteLLM. Cómo consiguió permiso para publicar lo explicó LiteLLM a las 05:22 del 22 de octubre, hora de Pekín; lo tienes más abajo en Actualización: la cadena de ataque que publicó LiteLLM.

Actualización: la cadena de ataque que publicó LiteLLM (22-10-2026)

Escrito el 22 de octubre de 2026 a las 06:56, hora de Pekín (22:56 UTC del 21). Hoy a las 05:22 hora de Pekín (21:22 UTC del 21), los mantenedores de LiteLLM publicaron una actualización en el issue #47826 con lo que creen que es la cadena de ataque completa. Según ellos, el atacante tuvo que saltarse varios controles de seguridad independientes. Aquí va paso a paso, tal como lo cuenta LiteLLM, que además avisa de que todavía está confirmando parte de lo que dice.

  1. Cómo llegaron los tokens a los logs: según LiteLLM, han observado situaciones concretas en las que Claude Code incluye el token OAuth de GitHub CLI del usuario en el cuerpo de una respuesta. Si se registran los cuerpos de las respuestas, esos tokens pueden acabar en los logs de LiteLLM. La actualización no explica cuáles son esas situaciones concretas.
  2. Qué logs: LiteLLM encontró una cuenta de usuario inesperada, con un nombre de lo más inocente, en un despliegue interno de LiteLLM expuesto a internet. LiteLLM cree que el atacante entró en ese despliegue y sacó de sus logs los tokens OAuth de GitHub CLI.
  3. De quién eran: LiteLLM dice que el atacante tenía las credenciales de GitHub CLI de tres ingenieros. Una podía ejecutar workflows de GitHub y otra pertenecía a uno de los dos owners de su organización de GitHub. Una actualización anterior (00:32 hora de Pekín) ya decía que el atacante lanzó project-releaser desde una cuenta y lo aprobó desde otra.
  4. El momento: LiteLLM cuenta que el lunes amplió temporalmente los permisos de publicación para que dos ingenieros internos pudieran sacar cada día backports de correcciones para clientes.
  5. Cambiar el flujo de publicación: el atacante intentó primero fusionar un cambio para que la herramienta de publicación pudiera apuntar a otro repositorio de origen, y las protecciones de rama lo bloquearon. Entonces usó el token del owner de la organización para desactivar esas protecciones y hacer un force push del cambio.
  6. Publicación: por último ejecutó el workflow modificado apuntando al repositorio imitador Berri-AI/litellm (el auténtico es BerriAI/litellm), que firmó y publicó los artefactos envenenados.

Conviene separar varias cosas:

  • Es la conclusión de LiteLLM, no algo que Anthropic haya confirmado. LiteLLM no explica cuáles son esas situaciones concretas, qué versiones de Claude Code están implicadas ni cómo reproducirlo. A las 06:56 hora de Pekín no he encontrado nada en @AnthropicAI, @claudeai ni en las páginas públicas de Anthropic: Anthropic todavía no ha respondido. Mientras tanto, no voy a presentarlo como una vulnerabilidad de Claude Code.
  • Cómo entró el atacante en ese despliegue interno no está confirmado. LiteLLM sospecha de una vulnerabilidad parcheada hace poco, pero no ha dicho cuál.
  • LiteLLM dice que la próxima actualización llegará a las 07:20 hora de Pekín (23:20 UTC). Hasta entonces sus registros de imágenes siguen bloqueados y las versiones normales están en pausa. Si hay novedades, las añadiré aquí.

Si usas Claude Code a través de un relay o de tu propia pasarela: cómo comprobarlo

Habrá que esperar para saber cómo se califica finalmente esta cadena. Pero si tu tráfico de Claude Code pasa por un relay de terceros o por una pasarela que gestionas tú, y en esa misma máquina tienes sesión iniciada en gh, estos pasos llevan poco tiempo:

  1. Ejecuta gh auth status para ver con qué cuentas de GitHub tiene sesión la máquina y qué permisos tienen los tokens.
  2. Invalida el token antiguo. No puedes ver los logs de un relay ni obligarle a borrarlos, así que lo seguro es dejar el token inservible: en GitHub, ve a Settings → Applications → Authorized OAuth Apps, busca GitHub CLI, pulsa Revoke y vuelve a ejecutar gh auth login. Hazlo en cada máquina donde tuvieras sesión.
  3. Si tienes tu propia pasarela, desactiva primero el registro de los cuerpos de peticiones y respuestas, y luego revisa los logs que ya tienes. En LiteLLM, mira turn_off_message_logging en litellm_settings y store_prompts_in_spend_logs en general_settings, y revisa también los callbacks de logging como Langfuse.
# lista los archivos de log que contienen un token de GitHub (muestra nombres, no tokens)
grep -rlE 'gh[opsu]_[A-Za-z0-9]{20,}' /ruta/a/logs-de-la-pasarela

# logs comprimidos
find /ruta/a/logs-de-la-pasarela -name '*.gz' -exec zgrep -lE 'gh[opsu]_[A-Za-z0-9]{20,}' {} +

El patrón cubre gho_ (el token OAuth que usa gh) y además ghp_, ghs_ y ghu_. Si los cuerpos se guardaron en una base de datos o en una plataforma de logs, busca gho_ también allí. Si aparece alguno, revoca primero el token en GitHub (para gho_, revoca la autorización de GitHub CLI como en el paso 2; para ghp_, bórralo en Personal access tokens), limpia después los logs que lo contengan y repasa el Security log de la cuenta por si hay algo que no reconozcas.

Fuente: la actualización de LiteLLM sobre la cadena de ataque en el issue #47826.

Por qué no te protegió que el código de GitHub estuviera limpio

Solemos dar por hecho que «el repositorio está limpio» y «el paquete está limpio» son lo mismo. No lo son. Lo que instalas es un artefacto compilado, un wheel o una imagen, que el pipeline de alguien generó a partir de cierto código. En este ataque LiteLLM, la etiqueta v1.107.0 de BerriAI/litellm de verdad está limpia. AISLE dice que su revisión automática «no encontró problemas en el código de LiteLLM», y Upwind, el equipo que abrió el issue, confirma que el código malicioso no está en la etiqueta.

La diferencia está en una línea del pyproject.toml del sdist que no aparece en la etiqueta: data = "cook" dentro de [tool.maturin]. Esa línea le dice a maturin, la herramienta de compilación Rust/Python que usa LiteLLM, que copie un directorio cook/ en la zona de datos del wheel. Lo que va ahí acaba en la raíz de site-packages. El directorio cook/ no está en el sdist ni en ninguna rama. JFrog señala que los archivos maliciosos tienen las mismas marcas de tiempo ZIP y los mismos metadatos que los legítimos, así que entraron durante una compilación normal y no se añadieron a mano después. Leyendo la etiqueta, o incluso abriendo el sdist, no habrías visto nada. Había que abrir el wheel.

Cronología del ataque LiteLLM, minuto a minuto

Todas las horas son de Pekín (UTC+8), con UTC entre paréntesis. Las del lado del atacante salen del análisis que hizo Upwind de los commits y de la procedencia de las imágenes en su primera actualización. El resto, de las marcas de tiempo de cada fuente.

Cronología del ataque LiteLLM en hora de Pekín, desde los commits del atacante a las 16:56 hasta la comprobación de registros de las 23:08
Elaborada a partir del issue #47826 (Upwind), AISLE, PyPI, Docker Hub y la API de GitHub. Hora de Pekín (UTC+8), UTC entre paréntesis.
  • 13 de octubre, 08:45 (00:45 UTC): se registra litellm.dev. Según Upwind, la cuenta de GitHub letters9813, que luego sirvió de buzón, se creó ese mismo día; AISLE dice que el repositorio del buzón apareció el 18. Lo prepararon con más de una semana de antelación.
  • 16:56–17:48 (08:56–09:48): nueve commits con autor Berri-AI <[email protected]> añaden los tres archivos maliciosos, devuelven la versión a 1.107.0 y meten data = "cook". Se hicieron desde el editor web de GitHub, por eso GitHub los marca como «Verified». Esa insignia solo significa que se hicieron en github.com. Puedes ver el diff completo: los commits están en la red de forks, sin rama ni etiqueta.
  • 18:46:29 (10:46:29): el décimo commit, 20535d8f, añade COPY cook ./cook a tres Dockerfiles. Lo comprobé con la API de GitHub: es el único de los diez sin firmar (verified: false), y su mensaje cita literalmente un fallo de compilación: «The gateway/backend/migrations builds failed with: maturin failed: No such data directory /app/cook».
  • 18:51:23 (10:51:23): cinco minutos después, la herramienta oficial de publicación BerriAI/project-releaser arranca la ejecución 39587059944. Según Upwind, su procedencia indica como origen github.com/Berri-AI/litellm en 20535d8f.
  • 19:39:51–19:40:13 (11:39:51–11:40:13): llegan ocho archivos a PyPI (AISLE).
  • 19:43:05: el firewall de dependencias de AISLE marca el primer wheel, 3 minutos y 14 segundos después de la subida. Fue interno; todavía no se hizo público nada.
  • ~19:48: en Docker Hub, v1.107.0, latest, main-latest y main-stable ya apuntan a la imagen envenenada.
  • 19:54:18: @UpwindMDR publica en X que ha visto un «posible compromiso de la cadena de suministro» y menciona cook.pth, sitecustomize.py y el sondeo de C2 cada hora. Por lo que he podido encontrar, es el primer aviso público: 14 minutos después de la subida y 12 antes del issue de GitHub.
  • 20:06:41 (12:06:41): el investigador de Upwind LidorMachluf abre el issue #47826 con todos los hashes, el análisis del código y los pasos de limpieza. El aviso de JFrog en X llega a las 20:22; cuando lo miré, hacia las 23:09, tenía 68.417 visualizaciones.
  • 20:42:54: un ejemplo de lo rápido que viaja una versión mala. El proyecto RAG VectifyAI/PageIndex fusiona el PR #619 y sube su mínimo a litellm>=1.107.0 para aprovechar un arreglo. Lo revierte a las 21:11:12 con el PR #620, titulado «1.107.0 was compromised». Durante 29 minutos, una instalación limpia desde esa rama main solo podía resolver a la versión envenenada.
  • 20:57: actualización de Upwind: las imágenes Docker también están infectadas, y la procedencia de todas las imágenes v1.107.0 apunta al repositorio imitador.
  • Como tarde, hacia las 21:12: PyPI pone la versión en cuarentena. Nadie ha publicado la hora exacta. La primera mención pública que encontré es un post en X de las 21:12:09, y la captura de PyPI que publicó un usuario a las 21:17 ya muestra «RELEASE QUARANTINED». AISLE dice que PyPI se puso en contacto «poco después de nuestro informe».
  • 21:50:33: primera respuesta oficial del mantenedor ojensen-berri: lo tratan como SEV-0 y «PyPI quarantined 1.107.0».
  • 22:32:11 (14:32:11): segunda respuesta: «The malicious docker images have been removed.» Los usuarios de código abierto deben fijar la 1.106.2. El registro privado para clientes enterprise no está afectado. Próxima actualización a las 16:30 UTC, es decir, a las 00:30 del 22 en hora de Pekín.
  • 23:08 y 23:18: nuestras propias pruebas de los registros (siguiente sección); la segunda, justo antes de publicar, dio el mismo resultado.
Cabecera del issue #47826 de GitHub y comentario del mantenedor a las 22:32, hora de Pekín, sobre la eliminación de las imágenes Docker: registro oficial del ataque LiteLLM
Fuente: issue #47826 de GitHub, BerriAI/litellm. Captura del 21 de octubre de 2026, 23:00 hora de Pekín (15:00 UTC). El «28m ago» del comentario corresponde a las 22:32.

«Eliminadas» eran las etiquetas: Docker Hub aún tenía la imagen

Este detalle del ataque LiteLLM es fácil de pasar por alto. A las 23:08, hora de Pekín (15:08 UTC), lancé peticiones HEAD de manifiestos contra los dos registros, y las repetí a las 23:18, justo antes de publicar, con idéntico resultado. No descargué ni ejecuté nada. En Docker Hub y en GHCR, latest, main-stable y v1.107.0 devolvían 404, y v1.106.2 devolvía 200. O sea, latest se borró, no se repuntó a una compilación limpia, y ahora un docker pull litellm/litellm sin etiqueta falla. No es malo: así es como debería fijarse siempre.

Pero en Docker Hub el índice envenenado (digest que empieza por sha256:1bbcb144…) y sus manifiestos amd64 (bd1aeee4…) y arm64 (56b74573…) seguían devolviendo 200. En GHCR esos mismos digests daban 404, así que allí sí se borraron. ¿Por qué importa? Si esta noche alguien fijó ese digest en un manifiesto de Kubernetes, un values de Helm o un compose, quizá porque «hizo lo correcto» y fijó el digest de lo que fuera latest en ese momento, seguirá descargando el ladrón. Compara tus digests con la lista de IOC de la actualización de Upwind. No publico a propósito el comando completo para descargarla.

Docker Hub ya no tiene etiqueta latest para litellm/litellm, pero una prueba muestra que el digest envenenado de la 1.107.0 aún devuelve 200 tras el ataque LiteLLM
Arriba: etiquetas en Docker Hub, captura a las 23:01 hora de Pekín. Abajo: peticiones HEAD de binmage a las 23:08 hora de Pekín (15:08 UTC); los digests están recortados a propósito.

Cómo funciona el código malicioso

Por qué un archivo .pth se ejecuta sin import

El módulo site de Python procesa al arrancar todos los archivos .pth de site-packages. Casi todas sus líneas solo añaden rutas, pero la documentación es clara: las líneas que empiezan por import seguido de espacio o tabulador se ejecutan con exec(), y «siempre se ejecutan al iniciar Python… tanto si el módulo se va a usar como si no». cook.pth ocupa 12 bytes: import cook. Así que cook.py se ejecuta cada vez que arranca ese intérprete o ese venv: tu aplicación, los tests, un kernel de Jupyter o el propio pip. Python 3.15 ha declarado obsoletas las líneas import en los .pth, pero durante ese periodo siguen ejecutándose, así que esta vía está abierta en todas las versiones que la gente usa de verdad.

sitecustomize.py es el segundo gancho. Después de preparar las rutas, Python intenta importar un módulo con ese nombre. Está pensado para que un administrador personalice la instalación; aquí se usa para lanzar el cargador.

Sección técnica de AISLE con cook.pth, cook.py y sitecustomize.py dentro del wheel 1.107.0, el truco central del ataque LiteLLM
Fuente: AISLE Research Team. Captura del 21 de octubre de 2026, 23:01 hora de Pekín (15:01 UTC).

Qué roba

cook.py es una sola línea con un bloque zlib+base64 que, descomprimido, son unos 61 KB de Python que se llama a sí mismo privcollect. Upwind publicó el código descodificado en un comentario posterior. Se ejecuta una vez en un hilo en segundo plano y no necesita servidor C2. Recoge:

  • información del equipo y de la red, historiales de shell, bases de datos y REPL, ~/.ssh y claves privadas GPG y TLS
  • contraseñas guardadas en navegadores Chromium (descifradas), cookies e inicios de sesión de Firefox
  • credenciales de AWS, Azure, GCP, Kubernetes, Docker, GitHub CLI y registros de paquetes, archivos .env*, *.pem y *.key
  • metadatos de la nube: credenciales del rol de instancia de AWS y de tareas ECS, que comprueba con STS GetCallerIdentity, además de tokens de GCP y Azure
  • archivos de configuración y sesión de herramientas de IA: Claude, Codex, Cursor, Gemini, Copilot, Windsurf, Kiro, tokens de Hugging Face y más
  • las variables de entorno de todos los procesos legibles en Linux (/proc/*/environ), no solo las suyas
  • si existe DATABASE_URL, un volcado de LiteLLM_CredentialsTable y LiteLLM_ProxyModelTable, es decir, todas las claves de proveedores que guarda el proxy

El paquete se cifra con AES-256-CBC, la clave se envuelve con una clave pública RSA-4096 incrustada y el resultado se envía por HTTP sin cifrar a http://www.litellm.dev/healthcheck. Una captura de red no te dirá qué se llevaron. Si el envío sale bien, escribe /tmp/.pc_once_72129118c59f5636c0bb. JFrog subraya lo que eso implica: que el marcador no esté no demuestra nada.

El cargador horario: dormido, no muerto

sitecustomize.py lanza un demonio separado del terminal (setsid, salida a /dev/null, directorio de trabajo /) y bloquea /tmp/remote_code_loader.lock. Cada 3.600 segundos lee un archivo domain.txt del repositorio de GitHub letters9813/pythonclient-test. Si contiene un dominio, vuelve a ejecutar el ladrón, esta vez sin marcador, y después descarga http://<dominio>/codepy y lo pasa directamente a exec() sin ninguna comprobación. En Windows el fork falla en silencio, pero cook.py se ejecuta igual.

Según Upwind, domain.txt está vacío desde el 20 de octubre, así que ninguna víctima recibió órdenes remotas. Cuando lo miré esta noche, tanto el repositorio como la cuenta letters9813 devolvían 404. Bien, pero no hay que leer demasiado en ello: el ladrón ya hizo su trabajo en el primer arranque, y nadie sabe todavía si ese nombre de usuario podría volver a registrarse. Que el C2 haya caído es «menos malo», no «seguro».

Cómo se publicó y qué está confirmado de verdad

Diagrama del ataque LiteLLM: etiqueta limpia, commits en la red de forks, repo imitador, wheels de PyPI y etiquetas Docker, cook.pth al arrancar Python, robo de datos y cargador horario
El paso del repositorio de origen es la conclusión de Upwind a partir de la procedencia de las imágenes; los mantenedores no lo han confirmado.

Esta es mi reconstrucción de cómo se publicó el ataque LiteLLM, separando pruebas de deducciones.

  • Confirmado por los artefactos: el código malicioso no está en la etiqueta; el pyproject.toml del sdist tiene la línea extra data = "cook"; los wheels y las imágenes contienen archivos maliciosos idénticos byte a byte.
  • Conclusión de Upwind, con pruebas sólidas, y la explicación posterior de LiteLLM coincide (ver la actualización del principio): la versión se construyó desde el repositorio imitador Berri-AI/litellm (fíjate en el guion). La procedencia SLSA de todas las imágenes v1.107.0 registra ese origen en 20535d8f, mientras que la misma herramienta registró el auténtico BerriAI/litellm para v1.106.2, v1.106.1 y v1.107.0-rc.2. En su actualización de las 21:24, Upwind además comparó byte a byte: el código del wheel 1.107.0 coincide con el commit de main a8113b0d (3.131 de 3.131 archivos), no con la etiqueta. También corrigió una afirmación suya anterior, que PyPI recibió la subida antes de la release de GitHub, porque resulta que ese es el orden normal de LiteLLM. Me gusta ver ese tipo de rectificación en un informe.
  • Mi deducción, no un hallazgo: el commit 20535d8f cita un fallo de compilación de Docker que solo aparece cuando existe data = "cook" pero el directorio no se copia, y la publicación oficial arrancó cinco minutos después. Eso apunta a que el atacante podía ver la salida de una compilación real, ya fueran los registros oficiales o una copia fiel del pipeline en otro sitio. Es una pista, no una prueba.
  • Ya tiene explicación: quién lanzó la ejecución 39587059944 y cómo su parámetro de repositorio de origen acabó siendo Berri-AI/litellm. Según LiteLLM, el atacante usó tokens robados de GitHub CLI para lanzar y aprobar la publicación, y el token de un owner de la organización para desactivar las protecciones de rama y hacer force push del cambio; ver la actualización de arriba. Desconocido: cómo entró el atacante en ese despliegue interno y cuánta gente descargó la 1.107.0. Ninguna fuente tiene un recuento de descargas por versión, y no voy a inventarme uno.
Commit del atacante 20535d8f, fuera de cualquier rama, que cita un error de compilación de Docker: prueba clave del ataque LiteLLM
Fuente: commit 20535d8f en GitHub. GitHub avisa de que no pertenece a ninguna rama del repositorio. Captura del 21 de octubre de 2026, 23:09 hora de Pekín.

Trusted Publishing y procedencia: qué tenía LiteLLM

PyPI admite Trusted Publishing, en el que un flujo de CI publica con una identidad OIDC de corta duración en lugar de un token guardado, y attestations, declaraciones firmadas con Sigstore sobre qué flujo compiló cada archivo. En el historial de versiones, todas las versiones de litellm muestran «No provenance», también la 1.106.2, la segura. Consulté además su endpoint de procedencia y devolvió 404. Upwind dice que litellm nunca ha publicado attestations.

¿Lo habrían evitado? Sinceramente, por sí solas probablemente no, si a la propia herramienta oficial de publicación le apuntaron al repositorio equivocado. Lo curioso es que las imágenes Docker sí llevaban procedencia SLSA, y así fue como Upwind detectó Berri-AI con guion. El registro existía; ninguna herramienta lo comprobó antes de desplegar. Según sus notas de versión, LiteLLM firma sus imágenes Docker con cosign. No he encontrado ninguna fuente que diga si las imágenes envenenadas tenían firmas válidas; si las tenían, solo demostrarían que «esto lo construyó nuestro pipeline», y el pipeline es justo lo que se manipuló.

Página de PyPI con litellm 1.106.2 como última versión y la 1.107.0 marcada como Release Quarantined y sin procedencia tras el ataque LiteLLM
Fuente: litellm en PyPI. Captura del 21 de octubre de 2026, 23:01 hora de Pekín (15:01 UTC).

Además, es el segundo compromiso de LiteLLM en PyPI este año. En marzo se publicaron las versiones 1.82.7 y 1.82.8 con un ladrón de credenciales después de que, según la propia LiteLLM, se filtraran credenciales a través de una dependencia comprometida de Trivy en su CI. JFrog dice que el código, la persistencia y la infraestructura de ahora son distintos y que no ha encontrado vínculo con aquel grupo, así que conviene tratarlos como incidentes separados. Aun así, dos compromisos de la vía de publicación en siete meses dicen mucho de lo difícil que es esta parte del trabajo.

Por qué una pasarela es el peor sitio para recibir el golpe

LiteLLM sirve para poner una sola API estilo OpenAI delante de decenas de proveedores. Por eso es tan popular entre los lectores de este sitio: pasas Claude Code o Codex CLI por un proxy de LiteLLM (por ejemplo, apuntando ANTHROPIC_BASE_URL a él) para usar modelos más baratos, repartir carga o estirar tu cuota de Claude. Esos montajes ahorran dinero de verdad, pero también significan que una sola máquina guarda a la vez tus claves de Anthropic, OpenAI, Gemini y Bedrock.

El ataque LiteLLM lo deja claro: este ladrón estaba hecho justo para esa máquina. Tiene un módulo que lee de la base de datos del proxy las claves guardadas, lee el entorno de todos los procesos y se lleva los archivos de sesión de Claude Code, Codex y Cursor del mismo equipo. Si el proxy corría en tu portátil, algo muy habitual en montajes personales, también se llevó las contraseñas del navegador y las claves SSH. Ese es el intercambio que quiero que se vea claro: un montaje de IA más barato concentra todas tus claves en un sitio, así que hay que tratarlo como algo importante.

El ataque LiteLLM también llega a través de los proyectos que dependen de él. Leí los archivos de dependencias de la rama main de cada uno. DSPy depende de litellm>=1.65.8 en su núcleo (con Python anterior a 3.15), así que una instalación nueva sin fijar esta noche habría resuelto a la 1.107.0. CrewAI (crewai[litellm]), Google ADK (su extra extensions), smolagents ([litellm]) y OpenAI Agents SDK ([litellm]) lo traen mediante extras opcionales sin límite superior. El paquete LLM de GraphRAG fija litellm==1.100.1, mem0 lo limita por debajo de 1.98.0, los requirements de aider fijan 1.82.3 y crawl4ai usa un paquete bifurcado, así que esos no estuvieron expuestos. «Resolvería a» no es lo mismo que «se infectó»; depende de tu lockfile y de cuándo instalaste.

Autocomprobación y limpieza tras el ataque LiteLLM

El orden importa. En un entorno infectado, cualquier arranque de Python vuelve a ejecutar el código malicioso, y pip es Python. Así que empieza por la shell.

# 1) Busca los archivos SIN arrancar Python
#    (cubre venv, conda, ~/.local y el dist-packages de Debian/Ubuntu)
find / \( -path '*site-packages*' -o -path '*dist-packages*' \) \
  \( -name cook.pth -o -name cook.py \) 2>/dev/null

# sitecustomize.py puede ser legítimo: mira qué contiene
find / -name sitecustomize.py 2>/dev/null | \
  xargs grep -l -E 'remote_code_loader|letters9813|litellm\.dev' 2>/dev/null

# 2) Marcadores en el host y el cargador en segundo plano
ls -la /tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock 2>/dev/null
fuser -v /tmp/remote_code_loader.lock 2>/dev/null   # o: lsof /tmp/remote_code_loader.lock
  1. Busca primero los archivos, como arriba. El patrón cubre site-packages y el dist-packages de Debian/Ubuntu, donde instala el pip del sistema. En un Mac, cambia / por ~, /usr/local, /opt y /Library/Frameworks para ir más rápido. Si encuentras cook.pth, da la máquina por comprometida.
  2. Docker: si hiciste pull de latest, main-latest, main-stable o v1.107.0 después de las 19:48 de esta noche, más o menos, da por hecho que te llegó. Las imágenes se quedan en tu equipo aunque el registro las borre; revísalas con los comandos que hay debajo de la lista.
  3. Luego revisa versiones con pip show litellm, pero solo en entornos donde el paso 1 no encontró nada.
  4. Si estás infectado: mata el proceso que tiene el archivo de bloqueo, borra con rm cook.pth, cook.py, sitecustomize.py y sus archivos de __pycache__, y a ser posible reconstruye el venv, el contenedor o la imagen desde cero. Desinstalar con pip no limpia bien: primero arranca Python, y un demonio que ya está corriendo sigue en memoria aunque borres los archivos.
  5. Cambia todo lo que la máquina podía alcanzar. Si tenía salida a internet, da por hecho que el envío funcionó. Empieza por todas las claves de proveedores guardadas en el proxy de LiteLLM y sigue con la master key y las virtual keys, la contraseña de DATABASE_URL, las claves y roles de la nube, las claves SSH y GPG, los tokens de GitHub, npm y PyPI, las sesiones de herramientas de IA (Claude Code, Codex, Cursor), las contraseñas guardadas en el navegador y cualquier secreto en el entorno de cualquier proceso de ese equipo.
  6. Vigila la factura. Revisa el consumo en los paneles de Anthropic, OpenAI y demás proveedores por si hay picos raros, y pon límites de gasto estrictos si aún no los tienes. Si usas AWS, el issue recomienda buscar en CloudTrail llamadas a GetCallerIdentity del rol de instancia en torno a la hora de instalación y cualquier uso de esas credenciales desde fuera de tu red.
# Qué imágenes de LiteLLM tienes en local y de cuándo son
docker images --digests | grep -i litellm

# Dentro de un contenedor del proxy en marcha (ls, no python):
docker exec <contenedor> sh -c 'ls /app/.venv/lib/python3*/site-packages | grep -E "^cook|^sitecustomize"'

Lecciones que durarán más que este incidente

Nada de esto exige herramientas nuevas. Casi todo lo que habría frenado el ataque LiteLLM es higiene aburrida que puedes dejar lista esta semana.

  • Fija versiones y hashes. pip install --require-hashes, o un lockfile de uv (uv lock) o poetry. Ojo: un hash demuestra que tienes el mismo archivo que fijaste, no que el archivo esté limpio. Los hashes de la 1.107.0 también eran «oficiales».
  • Fija imágenes por digest, nunca latest. Y apunta qué digest aprobaste, porque esta noche ha quedado claro que borrar una etiqueta no es borrar la imagen.
  • Espera un día antes de actualizar. Esta noche la detección tardó 3 minutos y el aviso público, 14. Un retraso de 24 horas en las actualizaciones que no son de seguridad (el minimumReleaseAge de Renovate, o simplemente no usar -U en CI) habría dejado fuera de esta ventana a casi todo el mundo.
  • Fijar la 1.106.2 es confianza, no verificación. Tampoco tiene attestations. Hasta que LiteLLM publique procedencia, «conocida como buena» significa «nadie ha encontrado nada todavía». Es un motivo para pedir al proyecto Trusted Publishing, no para entrar en pánico.
  • Trata tu pasarela como una caja fuerte. Usa claves de proveedor exclusivas para la pasarela, con límites de gasto bajos. Ejecútala en su propia máquina o contenedor, no en el portátil donde tienes el perfil del navegador y las claves SSH. Limita su tráfico de salida a las APIs que necesita. Un ladrón que no llega a litellm.dev no puede enviar nada.
# requirements.txt y luego: pip install --require-hashes -r requirements.txt
litellm==1.106.2 \
  --hash=sha256:cb1a2a1e55a4a8350ae2167525dc0d06f59b078cd685fb3e0f5419f02da5420e \
  --hash=sha256:34bfdc26c2c075726eda59ec0675e2a8a67b6e59a03980def7ef7651b849141a
#  (wheels manylinux x86_64 y aarch64; los hashes de los otros 6 archivos están en PyPI)

# Docker: etiqueta más digest
ghcr.io/berriai/litellm:v1.106.2@sha256:7294b90f868c5c38c99b83021c7ba46f9b439e95f8e32964c925c7d2563a03f7

Preguntas frecuentes

Solo uso el SDK de Python de LiteLLM, no el proxy. ¿Me afecta?

Sí, si se instaló el wheel 1.107.0. El código malicioso no depende de importar litellm ni de arrancar el proxy. Lo único exclusivo del proxy es el volcado de la base de datos, que necesita DATABASE_URL.

Ya desinstalé la 1.107.0. ¿Estoy limpio?

No necesariamente. Ejecuta los find de arriba, comprueba si hay un cargador en marcha y cambia las claves de todos modos. El ladrón actúa en el primer arranque, y desinstalar no te devuelve lo que ya se llevó.

¿La 1.106.2 es segura?

Upwind revisó todos los wheels de la 1.106.2 y de varias versiones anteriores y los encontró limpios, y los mantenedores recomiendan fijarla. No tiene attestation en PyPI, así que es la mejor prueba disponible ahora mismo, no una garantía criptográfica.

¿Afecta a Claude Code o a Codex?

El paquete envenenado no modificó Claude Code ni Codex. Pero en su actualización de las 05:22, LiteLLM dice que en situaciones concretas Claude Code incluye el token de sesión de gh en el cuerpo de una respuesta, donde los logs de la pasarela pueden guardarlo, y lo señala como el punto de partida del ataque. No ha explicado cuáles son esas situaciones y Anthropic todavía no ha respondido; ver la actualización de arriba, que también incluye los pasos para comprobarlo. Aparte de eso, si el ladrón se ejecutó en la misma máquina, leyó sus archivos de sesión y configuración. En ese caso, cierra sesión, vuelve a entrar y revoca las claves de API que tuvieras guardadas ahí.

¿Cuánta gente se vio afectada?

No se sabe. Ninguna fuente ha publicado cuántas veces se descargó la 1.107.0, y los datos robados van cifrados con la clave del atacante, así que nadie de fuera puede contarlos. Desconfía de cualquier cifra de víctimas del ataque LiteLLM que veas hoy.

¿Qué conviene vigilar ahora?

Los mantenedores dicen que la próxima actualización en el issue #47826 llegará a las 07:20 del 22 de octubre, hora de Pekín (23:20 UTC). La actualización de las 05:22 ya dio su versión de cómo secuestraron la vía de publicación (ver la actualización de arriba). Lo que sigue abierto es cómo entró el atacante en ese despliegue interno. Hasta que haya respuesta y vuelvan las versiones normales, yo trataría cualquier versión nueva de LiteLLM con más recelo y esperaría antes de actualizar.

Fuentes: issue #47826 de GitHub (informe de Upwind y respuestas de los mantenedores), AISLE, JFrog, Aikido, PyPI, Docker Hub, documentación del módulo site de Python, OSSF malicious-packages #1655. Comprobación de registros hecha por binmage a las 23:08 y las 23:18, hora de Pekín, del 21 de octubre de 2026.

Deja una idea

Tu dirección de correo no se publicará.