En marzo de 2026, un actor llamado TeamPCP comprometió en cadena varios proyectos open source ampliamente usados en pipelines de seguridad y CI/CD. Empezó con Trivy, el escáner de vulnerabilidades de Aqua Security, y terminó cinco días después con litellm, un gateway popular para llamar a APIs de LLMs. El reporte público de Arctic Wolf estima al menos 1,000 ambientes SaaS empresariales potencialmente afectados. Lo más incómodo del caso: muchas víctimas tenían “versiones pinneadas”. Solo que las habían pinneado mal.

Este post se enfoca en una forma de defensa específica contra ataques de supply chain: el version pinning estricto. Hay muchos otros vectores que conviene considerar, pero vamos a profundizar en uno que fue parte de cómo escaló este ataque.

¿Qué es un ataque de supply chain?

Un ataque de cadena de suministro de software no te ataca a ti directamente. Compromete algo de lo que tu código depende — una librería de PyPI, una GitHub Action, una imagen de Docker base — y deja que tú mismo lo ejecutes dentro de tu pipeline. Cuando esa dependencia corre en tu CI, tiene los mismos permisos que tu pipeline: tus secretos, tus tokens de cloud, tus credenciales de despliegue. Por eso un solo paquete comprometido puede impactar simultáneamente a miles de organizaciones.

El ataque de TeamPCP: de Trivy a litellm

19 de marzo, 17:43 UTC — Trivy. TeamPCP había conservado acceso residual a la organización aquasecurity desde una brecha anterior en febrero que no se rotó por completo. Con ese acceso, hizo force push a 76 de 77 tags de versión en aquasecurity/trivy-action, y los 7 tags de aquasecurity/setup-trivy. Cada tag — incluyendo versiones que llevaban meses estables — pasó a apuntar a un commit malicioso. Cualquier workflow en el mundo que usara uses: aquasecurity/trivy-action@v0.28.0 empezó a ejecutar un payload llamado “TeamPCP Cloud stealer”: volcaba memoria del proceso Runner.Worker, recolectaba secretos de SSH, AWS, GCP, Azure y Kubernetes, los encriptaba con AES-256 + RSA-4096 y los exfiltraba a un servidor remoto.

24 de marzo, 10:39 UTC — litellm. Aquí cambia el vector. En vez de comprometer GitHub Actions, TeamPCP publicó directamente dos versiones maliciosas (1.82.7 y 1.82.8) en PyPI, usando credenciales que sacó del compromiso de Trivy (litellm usaba trivy-action en su CI). La versión 1.82.8 incluyó un archivo litellm_init.pth que se ejecuta en cualquier arranque de Python del sistema — no solo cuando importas litellm.proxy. Las versiones estuvieron vivas en PyPI durante casi tres horas antes de ser puestas en cuarentena.

El eslabón roto: tags mutables

GitHub Actions y casi todos los package managers manejan la noción de “versión” como un alias. Cuando escribes uses: aquasecurity/trivy-action@v0.28.0, no estás pidiendo un commit específico; estás pidiendo “lo que sea que v0.28.0 apunte ahora mismo”. Un tag es un puntero, y los punteros son reescribibles por cualquiera con permisos de push y --force.

La única referencia inmutable de verdad en Git es el commit SHA. Si tu workflow referencia algo por tag o rama, no está pinneado — y GitHub mismo lo recomienda así en su documentación oficial sobre uso seguro de acciones.

La defensa, parte 1: GitHub Actions por SHA

Reemplaza esto:

- uses: aquasecurity/trivy-action@master
- uses: aquasecurity/trivy-action@v0.28.0

Por esto:

- uses: aquasecurity/trivy-action@18f2510ee396bbf400402947b394f2dd8c87dbb0 # v0.29.0

El comentario # v0.29.0 es para que humanos sepan qué versión es. Lo que GitHub Actions resuelve es el SHA, y el SHA es inmutable.

Aplica este principio a todas las acciones, no solo a las “de seguridad”. Una acción que parece inocua — actions/setup-node, actions/cache, una notification de Slack — corre con los mismos permisos del workflow y puede leer tus secretos.

GitHub agregó en agosto de 2025 una policy que rechaza ejecutar workflows que no usen acciones pineadas a SHA. Está disponible a nivel enterprise, organización o repositorio — actívala donde puedas. No depende de la disciplina del equipo, es un guardarraíl.

La defensa, parte 2: pip con --require-hashes

Para Python, pinear la versión (litellm==1.82.6) no es suficiente. La defensa real es hash-checking mode.

Genera un lock con hashes de todo el árbol:

pip-compile --generate-hashes --output-file=requirements.txt requirements.in

Esto produce:

litellm==1.82.6 \
    --hash=sha256:a8f3c4b8e7d6...
httpx==0.27.2 \
    --hash=sha256:5a7b3c8f9e1d...

Y en tu workflow:

- run: pip install --require-hashes --no-deps -r requirements.txt

--require-hashes hace que pip falle ruidosamente si cualquier paquete no tiene hash declarado o no matchea el contenido del wheel. ¿Te habría protegido esto contra el litellm hack? Sí. Con 1.82.6 pinneada con su hash, cualquier versión distinta que pip intentara instalar habría fallado — el hash no coincidiría.

Conclusión

Lo que TeamPCP demostró no es una vulnerabilidad técnica novedosa. Es que la industria confía tags y números de versión como si fueran inmutables cuando no lo son. La gente que usa Trivy lo usa precisamente porque le importa la seguridad — y aún así, la gran mayoría de pipelines comprometidos lo estaban porque pinneaban a @v0.28.0 en vez de a un SHA de 40 caracteres.

Un buen siguiente paso, hoy: abre tu .github/workflows/ y cuenta cuántos uses: apuntan a tag o rama. Migra cada uno a un commit SHA — es lo que te va a proteger cuando aparezca el próximo ataque de este tipo.