Cómo proteger PR externos en GitHub Actions: pull_request, permisos y aislamiento

Conoce los límites de permisos para Pull Requests externos en GitHub Actions y configura de forma segura pull_request_target, GITHUB_TOKEN y el aislamiento de flujos.

Al añadir etiquetas automáticas, comentarios de bienvenida o bots de revisión a GitHub Actions, puede surgir un problema de permisos: un flujo normal con pull_request no puede acceder a ciertos secretos del repositorio ni disponer de permisos de escritura. Algunas personas cambian el evento a pull_request_target. Esto ejecuta el flujo en el contexto privilegiado del repositorio de destino. Si después se descarga y ejecuta código del colaborador, un cambio malicioso podría leer secretos, modificar el repositorio o contaminar una caché.

La clave es separar la ejecución de código no confiable de las operaciones privilegiadas. Esta guía explica las diferencias entre eventos, el principio de mínimo privilegio, las combinaciones peligrosas, los patrones seguros y cómo validarlos.

Distinguir el contexto del evento del origen del código

El evento de GitHub Actions determina el contexto visible, los permisos del token y los secretos disponibles. Para Pull Requests de colaboradores externos, las opciones habituales son pull_request y pull_request_target. Elige según el trabajo que haya que realizar, no solo según si el flujo necesita comentar en el PR.

Evento Código descargado de forma predeterminada Permisos habituales para PR de forks Uso adecuado
pull_request Un commit de fusión para pruebas Normalmente limitados; los PR de forks no reciben secretos del repositorio Compilación, pruebas y análisis estático
pull_request_target El contexto de la rama base Permisos del repositorio de destino; puede incluir escritura y acceso a secretos Leer metadatos del PR, añadir etiquetas y comentar
workflow_run Un flujo posterior se inicia al terminar otro El flujo posterior puede tener más permisos Procesamiento posterior controlado

El código ejecutado con pull_request puede incluir pruebas, scripts de compilación y configuración de dependencias controlados por el colaborador. Trata el entorno del ejecutor como expuesto a entradas no confiables. La restricción de permisos forma parte del diseño de seguridad; no añadas secretos de larga duración para facilitar las pruebas.

La definición del flujo pull_request_target procede de la rama predeterminada del repositorio de destino y se ejecuta con su contexto. Puede leer metadatos del PR y resulta útil para añadir etiquetas o comentarios. Como también puede tener un token de escritura y acceso a secretos, nunca descargues una rama de un fork para ejecutar su código en ese flujo.

workflow_run tampoco es automáticamente una «segunda fase» segura. Puede ejecutarse con permisos elevados, mientras que los artefactos, las cachés y los archivos del flujo anterior pueden haber sido modificados por código no confiable. El flujo posterior solo debe leer datos definidos y validados; no ejecutes un artefacto del flujo anterior como si fuera un script de confianza.

Configurar explícitamente el token de solo lectura para las pruebas

Establece como solo lectura los permisos predeterminados de GITHUB_TOKEN en la configuración del repositorio. Después, declara los permisos precisos de cada flujo o trabajo. Así, un flujo nuevo no heredará accidentalmente permisos amplios de escritura.

Este flujo ejecuta comprobaciones de CI sin escribir en el repositorio:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
name: CI

on:
  pull_request:
    branches: [main]

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - name: Run tests
        run: ./scripts/test.sh

contents: read permite leer el contenido del repositorio. persist-credentials: false evita que checkout guarde el token en la configuración local de Git, algo apropiado si las pruebas no necesitan hacer push. La versión de Action del ejemplo facilita la lectura; en repositorios de producción, puedes fijar las dependencias a un SHA de commit completo conforme a tu política y mantenerlas actualizadas.

Si una Action no necesita la API de GitHub, puedes omitir permissions en el trabajo después de confirmar que los permisos predeterminados del repositorio están restringidos. Es más fácil auditar un flujo de solo lectura que concede permisos a trabajos concretos que dar contents: write a todo el flujo.

Mantener los secretos fuera de las pruebas de PR externos

Las pruebas normalmente no deberían depender de credenciales de despliegue de producción. Separa los flujos por finalidad: ejecuta pruebas sin secretos para los PR externos y despliega después de que el cambio llegue a una rama protegida, usando la aprobación de un Environment para acceder a las credenciales.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
name: Deploy

on:
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - name: Authenticate to cloud provider
        run: ./scripts/cloud-login.sh
      - name: Deploy
        run: ./scripts/deploy.sh

Si tu proveedor de nube admite OIDC, prefiere credenciales de identidad de corta duración en lugar de guardar secretos de nube de larga duración en el repositorio. id-token: write permite solicitar un token de identidad OIDC; no concede permiso para escribir contenido del repositorio. El proveedor de nube también debe limitar la confianza a la organización, el repositorio, la rama o el Environment previstos. Configura las condiciones de audience y subject según la documentación del proveedor y verifica ambos lados de la relación de confianza.

Las pruebas normalmente no necesitan credenciales de producción. Ejecuta las pruebas de PR externos sin secretos y reserva las credenciales de despliegue para el flujo que se ejecuta después de llegar a una rama protegida, con aprobación del Environment.

Usar pull_request_target para modificar metadatos de PR

Puedes usar pull_request_target para automatizar metadatos, siempre que el trabajo solo opere con datos del evento, como el número, el título o las etiquetas del PR. No debe descargar ni ejecutar contenido del PR, ni insertar campos no confiables en un comando de shell.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
name: Label pull request

on:
  pull_request_target:
    types: [opened, reopened]

permissions:
  pull-requests: write
  issues: write

jobs:
  label:
    runs-on: ubuntu-latest
    steps:
      - name: Add triage label
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          REPOSITORY: ${{ github.repository }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: gh pr edit "$PR_NUMBER" --repo "$REPOSITORY" --add-label "needs-triage"

El token queda limitado a pull-requests: write e issues: write, necesarios para gestionar etiquetas. Algunas operaciones de API requieren otros permisos. Consulta la documentación de la API de GitHub y añade solo los permisos que falten, en lugar de concederlos todos.

Pasa los valores de las expresiones mediante variables de entorno y luego como argumentos entrecomillados. Así no insertas texto controlado por el colaborador en el código del shell. Evita este patrón:

1
- run: echo "PR title: ${{ github.event.pull_request.title }}"

El colaborador controla el título del PR, por lo que insertarlo directamente en un script puede permitir una inyección de comandos. Trátalo como entrada no confiable incluso si solo quieres mostrarlo. Usa una API segura o una Action específica y limita los valores que llegan a los scripts.

No descargues código del PR en pull_request_target

No combines un evento privilegiado con una configuración como esta:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
on:
  pull_request_target:

jobs:
  unsafe:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - run: npm install && npm test

Esta configuración descarga un commit externo y ejecuta su instalación de dependencias y sus pruebas con permisos del repositorio. Un script malicioso de una dependencia podría leer variables de entorno, robar credenciales accesibles o escribir en el repositorio. Ocultar el token en otro paso no lo soluciona: los pasos de un trabajo comparten el entorno y el código puede acceder a variables de proceso o archivos.

persist-credentials: false reduce el riesgo de que las credenciales de checkout queden en la configuración de Git. No convierte un flujo privilegiado en un entorno aislado ni revoca los permisos disponibles para un proceso. No lo uses para justificar la ejecución de código no confiable con pull_request_target.

No hagas checkout del head de un fork ni ejecutes sus comandos bajo pull_request_target. El código malicioso podría acceder a los permisos y secretos disponibles en el trabajo. persist-credentials: false no convierte ese trabajo privilegiado en un entorno aislado.

Separar los flujos cuando sea necesario inspeccionar archivos del PR

Algunas automatizaciones necesitan analizar el diff o los resultados de compilación de un PR y luego publicar el resultado. Ejecuta primero el código no confiable en un flujo de solo lectura y sin secretos. Después, otro flujo restringido debe consumir únicamente resultados definidos.

Un diseño más seguro ejecuta pruebas con pull_request, sube solo un archivo de resultados pequeño y sencillo, y permite que un segundo flujo lo lea tras verificar las condiciones. El segundo flujo no debe ejecutar scripts, binarios ni objetos serializados sin validar que haya subido el primero.

El ejemplo muestra la separación del flujo de datos; no es un diseño de escalada de privilegios listo para usar:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
name: PR checks

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read

jobs:
  checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - run: ./scripts/test.sh
      - name: Write summary data
        if: always()
        run: printf '{"result":"%s"}\n' "${{ job.status }}" > result.json
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: check-result
          path: result.json
          retention-days: 1

Hay que validar el nombre del Artifact, su relación con el PR y el ID de ejecución, su procedencia, el tamaño del archivo y el análisis de JSON. Un archivo no es confiable solo porque se llame result.json: el código del colaborador puede influir en los datos que sube el flujo del PR.

Si el flujo posterior usa workflow_run, restringe workflows y types. Confirma que la ejecución pertenece al flujo esperado del repositorio de destino y valida el número del PR, el SHA del commit relacionado y el origen del Artifact. Reduce los permisos del trabajo posterior, no descargues el PR anterior y no ejecutes código del Artifact en el mismo trabajo. Para escrituras de alto riesgo, exige que un mantenedor lo inicie o que se apruebe el Environment.

Si no puedes validar de forma fiable el origen y el contenido del Artifact, no concedas permisos de escritura automáticamente. Mantener CI en solo lectura y pedir a un mantenedor que publique un comentario o haga la fusión tras las comprobaciones es una alternativa razonable.

Separar los trabajos de escritura y exigir aprobación

No compartas un token de escritura con todos los pasos de CI. Separa pruebas, compilación, publicación de estados y despliegue en trabajos distintos. Declara permisos únicamente en el trabajo que los necesita y haz que dependa de las comprobaciones previas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
jobs:
  test:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - run: ./scripts/test.sh

  publish-status:
    needs: test
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      statuses: write
    steps:
      - name: Publish status
        run: ./scripts/publish-status.sh

needs espera a que el trabajo anterior termine correctamente, pero no es una comprobación de confianza. Revisa las condiciones junto con el evento y la protección de ramas. No permitas que una entrada controlada por un fork haga pasar un flujo como si procediera de una rama de confianza.

Para despliegues de producción, configura un Environment llamado production en el repositorio, con revisores obligatorios, ramas autorizadas y secretos de entorno. El trabajo de despliegue solo podrá acceder a ellos tras la aprobación. La aprobación del Environment protege el acceso a sus secretos, pero no aísla el código no confiable que se ejecute en el mismo trabajo. No descargues código de un PR de fork en el trabajo de despliegue.

Revisar Actions de terceros y cachés

Cada Action referenciada con uses: se ejecuta en el Runner y puede acceder a sus entradas, variables de entorno y contexto del token. Aunque el YAML no pase GITHUB_TOKEN de forma explícita, una Action podría obtenerlo del contexto de GitHub. Revisa su código y versión, y limita los permisos del token.

En flujos críticos, fija las Actions de terceros a un SHA de commit completo y verifica que pertenece al repositorio de origen de la Action. Etiquetas como @v4 son legibles, pero pueden cambiar. Fijar el SHA mejora la reproducibilidad y exige mantener un proceso para aplicar actualizaciones de seguridad. Si el repositorio usa los archivos de bloqueo de dependencias de flujos de GitHub, revisa y actualiza las dependencias mediante ese mecanismo.

Una caché también puede transportar datos entre límites de confianza. No permitas que un flujo privilegiado ejecute contenido de caché generado por un PR no confiable. Tampoco dejes que un flujo no confiable sobrescriba una caché importante que usará después una rama de confianza. Incluye archivos de bloqueo o identificadores de commits confiables en las claves, valida el contenido tras restaurarlo y no trates la caché como un artefacto fiable.

Presta especial atención a las cachés compartidas con pull_request_target o workflow_run. La documentación de GitHub indica que estos eventos privilegiados pueden compartir cachés con la rama base. Si el código no confiable contamina una caché que luego utiliza un flujo privilegiado, el riesgo cruza el límite entre flujos.

Comprobar los permisos del token durante una ejecución

Tras integrar el cambio, no deduzcas los permisos efectivos solo a partir del YAML. Activa el flujo en una rama de prueba y revisa los permisos mostrados en la ejecución. También puedes añadir un paso que muestre solo los metadatos necesarios; nunca imprimas el token.

Por ejemplo, usa GitHub CLI para consultar metadatos del repositorio sin exponer la credencial:

1
2
3
4
5
6
7
8
9
- name: Inspect repository and event
  env:
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    REPOSITORY: ${{ github.repository }}
    EVENT_NAME: ${{ github.event_name }}
  run: |
    printf 'Repository: %s\n' "$REPOSITORY"
    printf 'Event: %s\n' "$EVENT_NAME"
    gh api "repos/$REPOSITORY" --jq '{full_name, visibility}'

Esta solicitud comprueba el acceso de lectura sin mostrar el token. Para verificar una escritura, usa el API correspondiente en un repositorio de prueba con los permisos mínimos y revisa la respuesta. No amplíes de forma permanente los permisos de producción para que pase una comprobación de diagnóstico.

CodeQL para GitHub Actions u OpenSSF Scorecard también pueden detectar patrones arriesgados, permisos del token y Actions sin fijar. El análisis estático señala riesgos, pero no sustituye la revisión de si el flujo ejecuta código no confiable o consume artefactos no confiables.

Lista de revisión

Antes de integrar un cambio en un flujo, comprueba lo siguiente:

  • ¿Las pruebas de PR de forks usan pull_request y no reciben secretos de producción?
  • ¿Los permisos predeterminados de GITHUB_TOKEN son de solo lectura y cada trabajo limita los suyos?
  • ¿pull_request_target solo gestiona metadatos, sin descargar ni ejecutar código del PR?
  • ¿Los títulos, nombres de rama y mensajes de commit se pasan mediante variables de entorno o argumentos seguros del API, y no se insertan en código de shell?
  • ¿workflow_run valida el flujo anterior, el ID de ejecución, el PR y el SHA del commit?
  • ¿El trabajo posterior solo lee datos limitados y validados, sin ejecutar archivos subidos?
  • ¿Se han revisado las Actions de terceros y se fijan versiones o SHA de commit?
  • ¿Un Runner autohospedado podría ejecutar un flujo de una rama no confiable?
  • ¿Una caché cruza el límite entre flujos no confiables y privilegiados?
  • ¿Los permisos de escritura y secretos de Environment solo están disponibles para los trabajos que los necesitan?

Proteger los propios archivos del flujo

Las definiciones de los flujos están en .github/workflows/. Si colaboradores externos pueden modificarlas y se integra automáticamente un PR solo porque pasan las pruebas, podrían eludir el diseño de permisos. Añade CODEOWNERS para ese directorio y exige que mantenedores revisen los eventos, permisos, origen de las Actions, expresiones y selección del Runner.

1
2
# .github/CODEOWNERS
/.github/workflows/ @your-org/security-reviewers

Sustituye el equipo de ejemplo por una cuenta o equipo que pueda realizar la revisión. Confirma que la protección de ramas exige la revisión de Code Owners y limita quién puede omitirla. Añadir CODEOWNERS sin activar la regla de protección correspondiente no exige aprobación por sí solo.

Revisa también action.yml, los flujos reutilizables, las Actions compuestas y los scripts de compilación. Un flujo puede parecer que solo ejecuta npm test, mientras que el package.json del repositorio asigna ese comando a cualquier script. Sigue toda la cadena de ejecución en lugar de buscar únicamente llamadas evidentes a curl o bash en el YAML.

Limitar los datos del evento al repositorio y la rama esperados

Si la automatización solo debe gestionar PR contra la rama predeterminada, indica explícitamente la rama base y comprueba que el evento procede del repositorio esperado. Los filtros de ramas reducen activaciones accidentales, pero no crean límites de permisos: un PR externo sigue conteniendo código no confiable y no debe ejecutarse en un flujo privilegiado.

1
2
3
4
on:
  pull_request_target:
    branches: [main]
    types: [opened, reopened, labeled]

Limita también los tipos de eventos. Si solo hay que etiquetar un PR al abrirse, no es necesario volver a ejecutar un flujo privilegiado en cada sincronización, cierre o comentario. Menos activaciones reducen oportunidades de error y hacen más comprensibles los registros de auditoría.

Al comprobar github.event.pull_request.head.repo.full_name, ten en cuenta la diferencia entre ramas del mismo repositorio y PR de forks. El campo puede estar vacío, por ejemplo, si se eliminó la rama de origen. Si falta un campo necesario, el flujo debe fallar de forma segura, no volver a permisos amplios. Trata como datos todas las cadenas del JSON del evento; no las concatentes en comandos de shell o rutas de archivos.

Si el flujo solo debe actuar en un repositorio determinado, comprueba también que base.repo.full_name coincide con el repositorio actual. En flujos reutilizables, revisa las entradas y permisos del llamador; no supongas que siempre es de confianza. Si hace falta, guarda una lista permitida en la configuración protegida de la rama predeterminada y revisa sus cambios.

Antes de publicar un comentario o etiqueta, valida que el número del PR sea un entero positivo, que el nombre del repositorio siga el formato owner/repo y que la etiqueta pertenezca a un conjunto fijo. Si la validación falla, detén el flujo y registra un motivo sin información sensible. Los conjuntos fijos para etiquetas y opciones de comandos evitan que una cadena del evento se interprete como argumento CLI.

Revisar el historial de ejecuciones y los cambios de permisos

La revisión de seguridad no debe limitarse a la configuración inicial. Inspecciona periódicamente el historial de Actions, sobre todo los permisos de escritura añadidos, el uso de secretos, las actualizaciones de Actions de terceros, los eventos pull_request_target y los cambios de etiquetas de Runner. Ante una ejecución inesperada, pausa primero las operaciones de escritura relacionadas y revisa el evento, el SHA del commit, las Actions invocadas y los registros.

Si aparece una credencial en los registros, elimina el registro o limita su acceso y rota la credencial inmediatamente. El enmascaramiento automático no está garantizado: valores codificados, concatenados o truncados quizá no coincidan con el secreto original. No pegues un token real en comandos de depuración, comentarios de PR o tickets; reproduce el problema en un repositorio temporal con un valor de prueba sin permisos.

Si varios mantenedores revisan flujos privilegiados, registra el propósito del cambio, por qué se necesita cada permiso, qué entradas son no confiables, si se descarga código del PR y cómo revertir el cambio. Ante permisos incorrectos, revoca primero los permisos de escritura innecesarios o desactiva el flujo; después determina si hay que rotar tokens y secretos de Environment.

Errores habituales y cómo resolverlos

El flujo de un PR de fork no puede leer un Secret

Es una protección de GitHub para contribuciones externas no confiables. No escribas el Secret en el archivo del flujo ni cambies el evento a pull_request_target para ejecutar el código del PR. Traslada el paso que necesita credenciales a un flujo posterior de una rama protegida o usa una configuración de prueba sin datos confidenciales.

La operación para etiquetar un PR devuelve 403

Comprueba que permissions incluya el permiso requerido, como pull-requests: write o issues: write, y que las políticas del repositorio u organización no restrinjan Actions. Añade solo el permiso que exija la API; no uses un token con todos los permisos para diagnosticarlo.

Un push de Actions no inicia el siguiente flujo

La mayoría de las escrituras realizadas con GITHUB_TOKEN no inician otra ejecución, para evitar ciclos recursivos. Revisa el diseño si depende de una cadena automática. Si hacen falta permisos adicionales, considera una GitHub App autorizada explícitamente y evalúa los riesgos de recursión, permisos y auditoría.

No uses un token personal para eludir esta limitación. Un PAT suele tener un alcance amplio, duración mayor y está vinculado a una cuenta personal. Si necesitas permisos adicionales, considera un token de instalación de GitHub App restringido al repositorio y los permisos necesarios.

La política de la organización impide los permisos de escritura

La configuración permissions del repositorio no puede sustituir una política de nivel superior. Revisa la configuración de Actions de la organización, las políticas del repositorio y la protección de ramas para conocer los permisos efectivos. Un administrador debe cambiar la política en el nivel adecuado; no intentes eludirla cambiando el evento.

Ejecutar PR externos en un Runner autohospedado

Un Runner autohospedado puede conservar el espacio de trabajo, cachés, conectividad de red y otras credenciales entre trabajos. En repositorios públicos, no ejecutes PR no confiables en un Runner que pueda acceder a la red interna o a secretos de larga duración.

Prefiere los Runners efímeros hospedados por GitHub. Si necesitas autohospedarlos, separa los grupos de Runners, limita qué repositorios y flujos pueden usarlos, elimina credenciales y accesos de red innecesarios y limpia el entorno después del trabajo. Borrar solo el directorio de trabajo no garantiza que el sistema vuelva a ser confiable.

Elegir el evento según el objetivo

Objetivo Evento sugerido Alcance del token Acciones que se deben evitar
Probar un PR externo pull_request Solo lectura de contenido Inyectar secretos de producción, conservar credenciales de Git
Etiquetar o comentar un PR pull_request_target Solo permisos necesarios de PR/Issue Descargar el head del PR, ejecutar scripts del PR
Desplegar una rama de confianza push a una rama protegida Lectura de contenido y permisos OIDC necesarios Ejecutar código de PR sin revisar en el trabajo de despliegue
Resumir un flujo anterior workflow_run con validación estricta Permisos mínimos de escritura Ejecutar artefactos anteriores o confiar en cachés compartidas

Si una automatización necesita tanto ejecutar código del colaborador como escribir en el repositorio de destino, divídela en dos fases de confianza. La primera realiza solo las pruebas necesarias para el código no confiable. La segunda escribe metadatos o despliega únicamente bajo condiciones controladas. Mantén pequeños y verificables los datos que pasan entre fases y analízalos como entradas no confiables.

Referencias