Inyección de scripts en GitHub Actions: qué demuestra realmente el caso de Snowflake

El texto de una incidencia no confiable se convirtió en código shell dentro del flujo de trabajo de GitHub Actions de Snowflake. La vía de inyección era convencional aunque un agente autónomo la encontrara y adaptara.

Comparte este artículo

El título de una incidencia pública de GitHub se convirtió en código shell ejecutable dentro de la automatización de un repositorio de Snowflake. Según la empresa de seguridad Wiz, su Red Agent autónomo encontró la vía, la utilizó para obtener una credencial interna de Jira e informó de ello a Snowflake el 23 de junio de 2026. El historial público del repositorio confirma que el flujo de trabajo se arregló ese mismo día; Wiz afirma que Snowflake también rotó la credencial.

La lección práctica no es que una IA derrotara a otra. Es que un flujo de trabajo de CI atravesó tres límites de confianza sin tratarlos como tales: una entrada pública llegó a un script generado, ese script se ejecutó en un worker privilegiado y el trabajo tenía una credencial para otro servicio.

Se trata de una inyección de scripts en GitHub Actions: datos controlados por un atacante se insertan en el código antes de que se inicie el shell del runner. Entrecomillar la expresión en YAML no lo soluciona. La solución fiable consiste en mantener los valores no confiables fuera del código generado y limitar después aquello a lo que podría acceder un runner comprometido.

Cómo ocurrió la inyección de scripts en GitHub Actions

La PR #1218 del conector de Snowflake cambió un flujo de trabajo que copiaba incidencias públicas de GitHub al sistema Jira de la empresa. La pull request se fusionó el 18 de junio de 2026. Su archivo jira_issue.yml gestionaba el título de esta forma:

run: |
  TITLE=$(echo '${{ github.event.issue.title }}' | sed ...)

Este fragmento está abreviado deliberadamente. No hace falta ningún payload de explotación para entender el fallo.

GitHub procesa las expresiones ${{ ... }} mientras prepara un trabajo. Su documentación sobre inyección de scripts explica que el valor resultante se sustituye en el script temporal antes de que el shell lo ejecute. Si el título de una incidencia pública contiene sintaxis shell que cierra la cadena entrecomillada, el comando sed posterior nunca llega a hacer que el valor sea seguro. El shell ya está interpretando código controlado por el atacante.

El desencadenante amplió la exposición. El flujo de trabajo se ejecutaba para las incidencias recién abiertas, de modo que cualquier usuario normal de GitHub podía proporcionar el título. Una expresión if parecía excluir a un bot, pero Wiz informa de que el campo de pull request referenciado era null para un evento de incidencia, lo que dejaba el trabajo abierto a cualquier autor de incidencias.

  1. Entrada pública

    Cualquier usuario de GitHub podía proporcionar el título de una incidencia.

    Control: Restringir el desencadenante.

  2. Expansión de la plantilla

    GitHub insertaba ese título en el script generado.

    Control: Usar una variable de entorno.

  3. Ejecución del runner

    El shell interpretaba el texto resultante como código.

    Control: Analizar los flujos de trabajo y entrecomillar los datos.

  4. Límite privilegiado

    El trabajo exponía credenciales de un servicio Jira interno.

    Control: Reducir el alcance del token y de la red.

La inyección abrió el límite del runner; las credenciales del trabajo y los servicios accesibles determinaron el radio de impacto. Este diagrama explicativo original se basa en el flujo de trabajo público y en la divulgación atribuida a Wiz.

El repositorio público confirma el cambio vulnerable y la reparación. La parte privada del incidente es menos visible de forma independiente. La divulgación de Wiz afirma que su agente adaptó una prueba de concepto fallida, obtuvo un token de Jira perteneciente a una cuenta de QA y validó el acceso de lectura en proyectos internos de ingeniería, cumplimiento y recompensas por errores. Wiz también afirma que los registros de auditoría de Snowflake no encontraron acceso de terceros durante la ventana de exposición de cinco días y que Wiz eliminó los datos a los que accedió. Esas conclusiones sobre el impacto y la investigación forense proceden de Wiz y Snowflake; al investigar este artículo no encontré un informe público independiente del incidente.

El patrón más seguro mantiene los datos fuera de la sintaxis shell

La corrección de Snowflake del 23 de junio restauró un límite basado en variables de entorno y utilizó jq para construir JSON:

env:
  ISSUE_TITLE: ${{ github.event.issue.title }}
run: |
  PAYLOAD=$(jq -n --arg title "$ISSUE_TITLE" '{summary: $title}')

Aquí, GitHub escribe el título en una variable de entorno en lugar de introducirlo en el programa shell generado. El script shell contiene el texto fijo "$ISSUE_TITLE", y las comillas shell normales pasan el valor como un único argumento. Después, jq --arg realiza la codificación JSON en lugar de una cadena de sustituciones construida manualmente.

Es fácil pasar por alto esta distinción: una variable de entorno es un límite de datos, no un desinfectante mágico. Un comando posterior todavía puede utilizarla mal. Entrecomilla las expansiones shell, pasa los valores como argumentos estructurados y evita evaluar cadenas construidas.

La referencia de uso seguro de GitHub recomienda una variable de entorno intermedia cuando una action dedicada no puede sustituir a un script en línea. Una action de JavaScript o compilada suele ser más sólida porque el valor no confiable llega como argumento y nunca participa en la generación del código fuente del shell.

Un linter de flujos de trabajo detecta el patrón mínimo

Para este artículo, reproduje el flujo de datos en dos archivos de flujo de trabajo desechables y ejecuté Zizmor 1.29.0 sin conexión. Su auditoría template-injection informó de la expresión directa ${{ github.event.issue.title }} dentro de run: como un hallazgo de inyección de código de alta confianza. La versión con variable de entorno no produjo hallazgos con el mismo comando.

Es útil, pero no constituye una garantía universal. Un linter puede reconocer patrones conocidos de origen a destino, pero no puede demostrar que cada script dinámico, action de terceros, alcance de token o ruta de red sea segura. Trata el análisis estático específico para flujos de trabajo como una señal de revisión necesaria, no como el veredicto final.

Una comprobación práctica de CI puede empezar con:

zizmor .github/workflows/

Ejecútalo sobre la representación final del flujo de trabajo fusionado, mantén controlada la versión de la herramienta y revisa las supresiones. Combínalo con la validación normal de YAML, el bloqueo de dependencias, el escaneo de secretos y pruebas que ejerciten el contexto específico de cada evento. Un flujo de trabajo de pull request y uno de incidencias no reciben payloads idénticos.

La historia de la IA es más limitada de lo que sugiere el titular

El historial público de commits no demuestra que la IA escribiera el cambio vulnerable. La PR #1218 contiene un commit separado con la coautoría de “Copilot Autofix powered by AI”, pero la revisión vulnerable de jira_issue.yml aparece en otro commit cuyo autor es el contribuidor humano. Wiz corrigió su artículo el 17 de agosto para decir que Copilot comprobó el cambio fusionado y dio el visto bueno, mientras que la autoría del cambio vulnerable no está clara.

Wiz también vende el Red Agent que encontró el problema. Su relato es un valioso informe primario del equipo de investigación, pero las afirmaciones sobre la autonomía del agente, su acceso privado a Jira y el alcance forense exacto deben seguir atribuyéndose hasta que se reproduzcan de forma independiente o Snowflake publique su propio informe.

La conclusión defendible es más útil que «IA mala frente a IA buena»:

  • El código asistido por IA recibe la misma revisión de seguridad que el código humano.
  • Un visto bueno automatizado es evidencia de una herramienta y una configuración, no una prueba de seguridad.
  • Las pruebas agénticas pueden explorar rápidamente una cadena de fallos, pero su resultado aún necesita verificación a nivel de artefacto y divulgación responsable.
  • Los revisores humanos necesitan un modelo de amenazas del flujo de trabajo: quién controla cada campo del evento, dónde se convierte en ejecutable y a qué puede acceder el runner.

Nuestra guía para leer evaluaciones de IA plantea el mismo punto de medición en otro contexto: una puntuación verde solo responde a la pregunta que la evaluación probó realmente. El Daily Digest del 18 de agosto contiene la breve cronología del incidente.

Qué observar a continuación

La pregunta sin resolver no es si la IA puede encontrar fallos en flujos de trabajo; este caso dice que puede hacerlo, al menos en una prueba comunicada por un proveedor. La cuestión más difícil es si los equipos añadirán análisis de flujos de trabajo consciente de los eventos y reducirán los privilegios del runner antes de que la próxima entrada pública llegue a un shell.

Para este incidente concreto, las pruebas de seguimiento útiles incluirían un informe posterior escrito por Snowflake, las comprobaciones de seguridad y los conjuntos de reglas exactos de GitHub aplicados a la revisión final de la PR, y un relato reproducible de forma independiente del proceso de decisión de Red Agent. Hasta entonces, el historial público del flujo de trabajo respalda una conclusión de ingeniería sólida, pero solo una conclusión matizada sobre la IA.

Fuentes

  1. Wiz disclosure of the Snowflake workflow vulnerability
  2. Snowflake connector PR 1218 and public workflow history
  3. Snowflake workflow remediation commit
  4. GitHub documentation on script injections
  5. GitHub secure-use reference for Actions
  6. Zizmor template-injection audit documentation