LiteLLM Fue Secuestrado. Dejé que Mi Agente de IA Instalara lo que Quisiera Durante 8 Meses. Esto es lo que Encontré.

9 min read

El jueves pasado, le pedí a Claude Code que me construyera una herramienta de transcripción. En 47 minutos, instaló 30 paquetes de Python en mi máquina. torch, scipy, huggingface_hub, numba, tiktoken. Y docenas de dependencias transitivas por debajo que ni siquiera conozco por nombre. Aprobé cada instalación sin mirar. Como siempre.

Luego veo que litellm fue comprometido. 97 millones de descargas por mes, dos versiones comprometidas en PyPI, un archivo .pth que se ejecuta en cada inicio de Python sin importar. Claves SSH robadas, tokens de AWS, credenciales de GCP, variables de entorno. Descubierto solo porque el malware tenía un bug que colgó la máquina.

No estaba usando litellm. Pero 59 paquetes instalados en pip global sin venv, sin lockfile, sin auditoría, después de 8 meses de desarrollo diario con un agente de IA? El próximo litellm venía por mí. Abrí mi terminal y miré qué había realmente en mi máquina. Esto es lo que encontré, y lo que arreglé en 45 minutos.

TLDR: Los agentes de programación con IA eliminaron la última fricción humana entre tú y los ataques de cadena de suministro. Ellos instalan, tú apruebas, nadie audita. Escaneé mi máquina, encontré 59 paquetes globales sin auditar, migré a uv con dependencias bloqueadas por hash, y agregué cuatro líneas a mi CLAUDE.md que restauran la fricción. Todo tomó 45 minutos. Abre tu terminal y ejecuta pip freeze antes de tu próxima sesión de código.

Dos desarrolladores explorando riesgos de dependencias de software con ilustración cómica
Cuando tu agente de IA se convierte en la rebelión definitiva del gestor de paquetes 🤖🏴‍☠️

Qué Le Pasó a LiteLLM (Y Por Qué Debería Asustarte)

El 24 de marzo, un grupo de amenazas llamado TeamPCP publicó las versiones 1.82.7 y 1.82.8 de litellm en PyPI a través de una cuenta de mantenedor robada. El mismo grupo había comprometido previamente Trivy, un escáner de seguridad (sí, la gente que hackea herramientas de seguridad para hackear tus herramientas). El payload era un archivo .pth. Si no sabes qué es eso: Python carga archivos .pth automáticamente al inicio. No necesita import. No deja rastro en tu código. Solo tener el paquete instalado era suficiente.

El malware cosechó claves SSH, credenciales de AWS, tokens de GCP, configuraciones de Kubernetes, variables de entorno, y wallets de crypto. Se comunicaba con un servidor externo en cada inicio de Python. 97 millones de descargas por mes en ese paquete.

Se descubrió porque el atacante cometió un error. El malware tenía un bug que causaba un fork bomb, el uso de RAM se disparó, las máquinas se colgaron. Un desarrollador usando Cursor notó que su máquina se estaba muriendo, lo rastreó hasta un plugin MCP que había descargado litellm como dependencia transitiva. Transitiva. Nunca instaló litellm él mismo. Su herramienta de IA lo hizo, a través de un plugin, a través de una cadena de dependencias que nunca revisó.

Una advertencia importante: la imagen oficial de Docker de litellm no se vio afectada (deps fijadas). El repo de GitHub se mantuvo limpio. Solo la distribución de PyPI fue comprometida. Pero ahí es donde la mayoría de la gente obtiene sus paquetes.

Si el atacante no hubiera colgado la máquina, nadie se habría dado cuenta. Por semanas. Tal vez meses.

Le Pedí a Mi Agente de IA Que Escaneara Mi Propia Máquina

Cuando leí sobre litellm, mi primera reacción fue verificar si había sido afectado. Mi segunda reacción fue darme cuenta de que tenía cero herramientas para hacerlo.

Así que hice lo que siempre hago. Le pregunté a Claude Code. "Escanea mi máquina por paquetes de Python comprometidos." Lo que siguió fue un poco vergonzoso de ver. El agente comenzó una búsqueda recursiva, inmediatamente tuvo timeouts porque iCloud estaba sincronizando la mitad de mi sistema de archivos. Symlinks ralentizando ripgrep por todas partes. Tuvo que navegar alrededor del sandboxing de macOS solo para mirar mis propios site-packages. Como ver a alguien tratar de inspeccionar su propia casa pero las puertas se siguen cerrando solas.

El escaneo encontró 59 paquetes instalados en pip global. Cincuenta y nueve. Reconocí tal vez la mitad de ellos. El resto eran dependencias transitivas de esos 47 minutos construyendo la herramienta de transcripción, más meses de otros momentos de "claro, instala eso" acumulados encima.

Luego le pedí a Claude Code que ejecutara pip-audit. Resulta que pip-audit no estaba instalado. Nunca lo necesité antes (nunca pensé en ello antes, más precisamente). Y aquí es donde se pone propiamente irónico: para instalar pip-audit, Claude Code tuvo que crear un venv temporal porque PEP 668 bloquea instalaciones globales en Python moderno. El mismo PEP 668 que los 30 paquetes de whisper habían evitado con --break-system-packages tres semanas antes.

La herramienta de auditoría fue bloqueada por el sistema. Los 30 paquetes sin auditar habían pasado directo.

pip-audit encontró un CVE. Pygments 2.19.2, severidad baja, una librería de renderizado no expuesta a la red. No litellm en mi máquina. Limpio.

Pero limpio por suerte, no por diseño. Y ahí es donde empieza el problema real.

Tu Agente de IA Eliminó la Última Fricción Que Te Protegía

Antes de los agentes de programación con IA, tú mismo escribías pip install. Era un momento consciente. Veías el nombre del paquete. Tenías al menos un segundo para preguntarte si ese era el correcto, si realmente lo necesitabas, si tal vez deberías verificar el número de descargas primero. La mayoría de las veces no verificabas. Pero la fricción estaba ahí. Un tope de velocidad entre tú y un potencial compromiso de cadena de suministro.

Los agentes de IA eliminaron ese tope completamente.

En mi máquina, puedo rastrear las dos olas de instalación del proyecto de transcripción. Primera ola a las 14:42, Claude Code probó openai-whisper. Segunda ola a las 15:45, cambió a mlx-whisper porque el primero no funcionaba bien en Apple Silicon. Dos árboles de dependencias completos superpuestos. No revisé ninguno. Probablemente estaba haciendo café durante el segundo.

Esto es fatiga de consentimiento aplicada a dependencias. El agente propone, tú presionas "y", los paquetes fluyen. El mismo mecanismo que te hace aceptar banners de cookies sin leerlos, excepto que el banner de cookies no puede robar tus claves SSH.

El comentario de Andrej Karpathy después de litellm fue directo: "yoink con LLMs, menos dependencias." Y tiene razón en principio. Menos superficie de ataque significa menos riesgo. Pero yoink no resuelve el problema de dependencias que no puedes eliminar. litellm no es una utilidad de 50 líneas que puedes copiar y pegar. Tampoco torch. Tampoco scipy. Algunas dependencias existen porque la alternativa es reescribir una década de código C optimizado, y nadie está haciendo eso entre dos reuniones un jueves.

Exploré el modelo CLAUDE.md de 3 capas (revisión, aplicación, intención) hace unos meses. Resulta que faltaba una capa: política de seguridad de dependencias. El modelo le dice al agente cómo escribir código, pero no dice nada sobre qué se le permite instalar.

Los agentes no crearon un nuevo vector de ataque. Los ataques de cadena de suministro existían mucho antes de Claude Code. Lo que hicieron los agentes fue eliminar la fricción que te protegía de ellos. Es un amplificador, no una causa. Pero la consecuencia es idéntica.

El agente no creó la vulnerabilidad. Eliminó la fricción que te protegía de ella.

Diagrama de cadena de confianza mostrando: Dev aprueba agente → agente llama pip install → pip resuelve árbol de dependencias (invisible para dev) → cada nodo en el árbol es un punto de ataque potencial. Contraste visual entre flujo "antes de agentes" (fricción consciente, dev ve nombre del paquete) y flujo "con agentes" (aprobación automática, árbol de dependencias oculto). Estilo geométrico plano.
Cadena de Confianza: Antes vs. Después de Agentes de IA

Qué Cambié en 45 Minutos (Y Qué Realmente Te Protege)

Lo primero que hice:

pip freeze > requirements-snapshot-20250325.txt

Eso es todo. Una instantánea de todo lo instalado globalmente actualmente. No es una solución. Es una fotografía de la escena del crimen antes de empezar a limpiar. Porque necesitas saber cómo se ve el desastre antes de tocarlo.

Luego revisé la instantánea y fijé cada versión exactamente. No más >=. No más ~=. Cada versión bloqueada a su número exacto. Si tienes requests>=2.28 en cualquier archivo en tu máquina ahora mismo, le estás diciendo a pip "instala cualquier versión que quieras mientras esté por encima de 2.28." Eso incluye una hipotética 2.32 comprometida que TeamPCP suba el próximo martes.

Siguiente: uv. Construido por Astral (la gente detrás de Ruff). Inicialicé mi proyecto de transcripción apropiadamente esta vez:

uv init transcription-tool
cd transcription-tool
uv add mlx-whisper typer

Dos dependencias directas. uv las resolvió en 63 paquetes. Instaló 42 (el resto son alternativas específicas de plataforma). Y la parte que importa: el lockfile contiene hashes SHA256 para cada paquete.

El hash es la protección real. Lo que pasó con litellm: TeamPCP subió una nueva versión con el mismo patrón de número de versión a PyPI. Si estabas usando pip con >=, lo habrías descargado automáticamente. Pero si estás usando un lockfile con hashes, el contenido del paquete no coincide con el hash registrado. uv sync se niega a instalar. Listo. Ataque bloqueado en la puerta.

pip no tiene un mecanismo equivalente por defecto. Puedes usar --require-hashes, pero tienes que generarlos y mantenerlos tú mismo. uv lo hace automáticamente en cada uv lock. Aclaración completa: uv es una herramienta joven (Astral la lanzó en 2024), y el ecosistema alrededor aún se está moviendo rápido. Pip con --require-hashes te da protección similar si prefieres algo más establecido. Lo importante es el lockfile con hashes, no la herramienta específica.

Último paso: aislamiento de venv por proyecto. Cada proyecto obtiene su propio entorno. No más pip global en una máquina que tiene claves SSH, tokens de API, y credenciales de nube sentadas en variables de entorno. También agregué .nosync para iCloud porque tener tu servicio de nube sincronizando tu venv mientras debuggeas conflictos de dependencias es el tipo de experiencia que no quieres tener dos veces.

Antes: 30 paquetes en pip global. Sin rastro de qué los instaló o cuándo.
Después: 2 dependencias. 63 resueltas. Cada una con un hash SHA256. En un venv aislado que no puede tocar el resto de mi sistema.

Las Cuatro Líneas Que Agregué a Mi CLAUDE.md

Las herramientas no son suficientes si el agente que instala paquetes para ti no tiene reglas. Así que agregué cuatro líneas al inicio de mi CLAUDE.md:

1. Nunca ejecutar pip/npm install sin aprobación explícita del usuario
2. Antes de sugerir una nueva dependencia, listar sus principales sub-dependencias
3. Preferir generar código utilitario sobre agregar una dependencia cuando la funcionalidad es menor a 200 líneas
4. Siempre fijar versiones exactas, nunca usar >= o ~=

Ya tenía "nunca instalar sin preguntar" en mi framework de contratos de prompts. Esa regla era necesaria pero no suficiente. Aún estaba aprobando a ciegas porque no podía ver el árbol de dependencias.

La regla 2 es la que restaura la fricción. Cuando Claude Code me dice "mlx-whisper necesita torch, numpy, scipy, huggingface_hub, tokenizers, y 25 dependencias transitivas," tengo un momento consciente otra vez. Puedo decidir si una herramienta de transcripción vale la pena descargar la mitad de PyTorch en mi máquina. Tal vez la respuesta sigue siendo sí. Pero al menos estoy decidiendo, no aprobando automáticamente.

La regla 3 es el principio de Karpathy con una barrera de protección. Si la funcionalidad es suficientemente simple, genera el código en lugar de instalar un paquete. Pero "suficientemente simple" necesita un límite, de otra manera terminas reescribiendo librerías de criptografía desde cero (por favor no lo hagas). Doscientas líneas es mi umbral. El tuyo podría ser diferente.

La regla 4 es la más básica y la más ignorada. Cada >= en tu proyecto es una puerta que dejaste abierta para que alguien más entre.

Estas cuatro líneas no son mágicas. Un agente suficientemente capaz podría evitarlas. Pero para los agentes de programación con IA de producción actuales, es la diferencia entre instalar a ciegas e instalar conscientemente. Estas reglas no protegen contra paquetes ya comprometidos en tus deps existentes. Previenen futuras instalaciones ciegas. Auditar lo que ya está ahí (pip-audit, uv lock) es un paso separado que aún necesitas hacer.

La fricción que tu agente eliminó, la devuelves con cuatro líneas en tu CLAUDE.md.


El próximo litellm viene en camino. No es una cuestión de "si." Y esta vez el atacante no cometerá un bug que colapse tu RAM. El malware será silencioso, la exfiltración silenciosa, y no sabrás que tus claves SSH se fueron hasta que veas commits que nunca hiciste.

Antes de que te golpee el próximo, abre tu terminal. Ejecuta pip freeze. Cuenta los paquetes que no reconoces. Instala uv. Crea un venv. Abre tu CLAUDE.md y agrega cuatro líneas antes de tu próxima sesión. 45 minutos. Costo cero.

Tu agente de programación con IA es exactamente tan seguro como las reglas que le das. Sin reglas, sin seguridad. Solo suerte. Y la suerte no es un plan.

Fuentes: Hilo de GitHub sobre el compromiso de cadena de suministro de LiteLLM: github.com/BerriAI/litellm/issues/9484 | Documentación de uv: docs.astral.sh/uv

(*) La portada es generada por IA. Mi salida real de terminal durante el escaneo era mucho menos fotogénica.