Permisos y Seguridad
Seguridad
La inyección de scripts trata sobre alguna secuencia de comandos que está siendo inyectada en el workflow desde fuera que puede ser usada para cosas maliciosas.
Vamos a imaginar que un workflow fuera disparado siempre que una nueva issue ocurriese y el título de la issue fuera utilizado en el workflow de alguna manera. Dependiendo de dónde inyectamos este valor y cómo inyectamos, si el título fuera una cadena representando un comando sería ejecutado.
Siempre verifica actions de terceros y la reputación de los mismos. La reutilización de código es algo bueno y malo al mismo tiempo. Una vez que actions ejecuta comandos, esos comandos pueden robar vuestras credenciales.
Cuida del permiso de acceso al repositorio y evita permisos excesivamente permisivos. Cuando crees un token para algo asegúrate que solamente tenga el permiso de hacer lo que es preciso y nada más allá de eso. En un flujo de trabajo que haga solamente pruebas sería más interesante que el workflow solamente tenga permiso de lectura.
Vamos a analizar este workflow.
name: Label Issue
on:
issues:
types:
- opened
jobs:
assign-label:
runs-on: ubuntu-latest
steps:
- name: Assign label
run: |
issue_title="${{ github.event.issue.title }}"
if [[ "$issue_title" == *"bug"* ]] then
echo "Issue is about a bug!"
else
echo "Issue is not a bug
fi"
Si tiene bug en el nombre entonces va a imprimir que es un bug sino va a imprimir que no lo es.
Si abrimos una issue con el título Something's wrong tenemos esto.

Pero si abrimos una issue con el título a";echo Got your secrets.

Lo que sucedió fue que el a" cerró el "$de la primera línea y después el resto ; echo Got your secrets fue ejecutado.
El motivo de que esto haya ocurrido es que ejecutamos ese comando en el shell del runner. Nuestro código permite eso. Cualquier cosa dentro de $() dentro del shell es interpretado. El código abajo no permitiría eso.
name: Label Issue
on:
issues:
types:
- opened
jobs:
assign-label:
runs-on: ubuntu-latest
steps:
- name: Assign label
env:
TITLE: ${{ github.event.issue.title }}
run: |
if [[ "$TITLE" == *"bug"* ]] then
echo "Issue is about a bug!"
else
echo "Issue is not a bug
fi"

Una de las maneras de defenderse y evitar el uso de interpolaciones dentro del código en el run y siempre que sea posible utilizar actions en vez del run.
Sobre actions primero busca actions desarrolladas por el equipo de github, después de socios verificados y solamente en último caso las públicas.
Procura verificar el repositorio de la action y comprobar el código.
Permiso
Hasta ahora ejecutamos los workflows sin preocuparnos por el permiso.
Vamos a analizar este workflow. Cada vez que una issue sea creada solamente va a ejecutar si tiene el nombre bug en la issue. Estamos usando la api github para establecer la etiqueta bug en esta issue.
name: Set Label Issue
on:
issues:
types:
- opened
jobs:
assign-label:
runs-on: ubuntu-latest
steps:
- name: Assign label
if: contains(github.event.issue.title, 'bug')
run: |
curl -X POST \
--url https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/labels \
-H 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \
-H 'content-type: application/json' \
-d '{
"labels": ["bug"]
}' \
--fail
Este workflow solamente necesita permiso para interactuar con issue pero no necesita permiso para pull request ni usar el código del repositorio.
Los permisos son a nivel de job no de workflow. Si definimos un permiso a nivel de workflow todos los jobs heredarán este permiso, pudiendo ser redefinidas dentro de cada job.
Estos son los permisos que podemos definir para un github token.
permissions:
actions: read|write|none
attestations: read|write|none
checks: read|write|none
contents: read|write|none
deployments: read|write|none
id-token: read|write|none
issues: read|write|none
discussions: read|write|none
packages: read|write|none
pages: read|write|none
pull-requests: read|write|none
repository-projects: read|write|none
security-events: read|write|none
statuses: read|write|none
Voy a colocar directo en el ejemplo algunas anotaciones
name: Set Label Issue
on:
issues:
types:
- opened
jobs:
assign-label:
# permissions: write-all # permiso completo Este es el permiso por defecto.
# permissions: read-all # permiso completo de visualización
permissions:
issues: write
# issues: read # Va a dar problema con 403
runs-on: ubuntu-latest
steps:
- name: Assign label
if: contains(github.event.issue.title, 'bug')
run: |
curl -X POST \
--url https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/labels \
-H 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \
-H 'content-type: application/json' \
-d '{
"labels": ["bug"]
}' \
--fail
Esta es una capa más de protección que podemos usar, pero aún así puede permitir una inyección de script.
Si el permiso fuera solamente de lectura no podríamos establecer una etiqueta con un permiso denegado de código 403.

Cada vez que un job es creado un token es generado para ese job y podemos acceder vía ${{ secrets.GITHUB_TOKEN }}.
Es posible cambiar la restricción de permisiva a restrictiva para las actions en settings. Esto garantiza que el permiso para los workflows necesitan ser dados correctamente. Vale la pena comprobar en la documentación.
No sé si es una buena idea dejar que github actions cree y apruebe pull requests a no ser que un buen gitflow sea utilizado en el repositorio.
Vale la pena echar un vistazo a pull requests viniendo de forks y tornarlos más restrictivos en caso de estar trabajando en un proyecto público. El valor por defecto es que si apruebas la primera vez los próximos serán aprobados automáticamente.