Dans notre précédent article, nous avons vu comment détecter et se protéger contre les empoisonnements directs Pipeline Exécution (D-PPE). Nous avons également vu comment détecter cette vulnérabilité en utilisant Scanner Xygeni, ainsi que certains mécanismes de protection.
Empoisonné Pipeline Exécution (EPI) est produit lorsque l'attaquant peut modifier le pipeline logique de deux manières :
- En modifiant le fichier de configuration CI (le pipeline) -> EPI direct (D-EPI)
- En modifiant les fichiers référencés par le pipeline (par exemple : scripts référencés depuis le pipeline fichier de configuration) -> EPI indirect (I-PPE)
Dans cet article, nous approfondirons les EPI indirects. Mais, avant cela, et en complément de mon article précédent, voyons d'abord comment GitHub gère l'exécution de pipelines et quels sont les mécanismes de protection contre le D-PPE.
Comment GitHub protège-t-il l'exécution de pipelineça vient des PR ?
Comment fonctionne GitHub concernant l'exécution de modifications pipelines?
Modifié pipelines peut provenir de Pushes ou Pull Requests (PR). En tant que bonne pratique majeure, il est fortement recommandé d'éviter toute « poussée » directe vers une branche protégée et d'utiliser Pull Requests comme mécanisme pour imposer un certain examen avant d'accepter tout code contribué.
Pull Requests peut provenir de deux sources différentes :
- PR provenant de Fourchettes
- PR provenant de branches
PR de Fourchettes peut provenir soit de public or Privé référentiels.
Comme nous avons affaire à des EPI (Poisoned Pipeline Exécution), notre point principal n’est pas « l’acceptation » d’un PR mais l’exécution d’un PR modifié. pipeline pendant le processus d'acceptation/approbation du PR. Au cœur d’une attaque EPI se trouve l’exécution involontaire d’un logiciel modifié « malveillant ». pipeline.
En quelques mots, Empoisonné Pipeline L'exécution (EPI) est produite lorsque l'attaquant peut modifier le pipeline logique.
Il y en a deux variantes:
- EPI direct (D-EPI) : Dans un scénario D-PPE, l'attaquant modifie le fichier de configuration CI dans un référentiel auquel ils ont accès, soit en poussant la modification directement vers une branche distante non protégée sur le dépôt, soit en soumettant un PR avec la modification depuis une branche ou un fork. Depuis l'IC pipeline l'exécution est définie par les commandes dans le fichier de configuration CI modifié, les commandes malveillantes de l'attaquant s'exécutent finalement dans le nœud de build une fois la build terminée. pipeline est déclenché.
- EPI indirect (I-EPI): Dans certains cas, la possibilité d'un D-PPE n'est pas disponible pour un adversaire ayant accès à un SCM référentiel (par exemple si le pipeline est configuré pour extraire le fichier de configuration CI d'une branche distincte et protégée dans le même référentiel). Dans un tel scénario, plutôt que d'empoisonner le pipeline lui-même, un attaquant injecte du code malveillant dans les fichiers référencés par le pipeline (par exemple : scripts référencés depuis le pipeline fichier de configuration)
Dans les deux cas, GitHub exécutera le fichier modifié pipeline sans avoir besoin d’un examen ou d’une approbation préalable.
PR à partir de Forks public repos
GitHub permet de configurer le comportement lors du traitement PR provenant de forks dans des dépôts publics.
Lorsqu'un PR provient d'un fork, GitHub force toujours un certain niveau « d'approbation » avant d'exécuter le PR. pipeline associé au PR. Ce niveau d’approbation va d’une approbation faible à une approbation stricte.
At Niveau de l'organisation (Org>>Paramètres>>Actions>>Général), vous pouvez choisir parmi plusieurs options « approbation » :
Le plus strict est le dernier («Exiger l’approbation de tous les collaborateurs externes»), car GitHub nécessitera toujours une approbation lorsque le PR provient de forks de collaborateurs extérieurs.
Mais même dans ce cas strict, il existe différences entre les collaborateurs disposant d'autorisations de lecture et d'écriture.
- Lorsque le PR vient d'un lire utilisateur, le exécution de la pipeline est arrêté jusqu'à ce que les changements soient approuvés. Si l'approbation est correcte, alors le modèle modifié pipeline est exécuté.
- Lorsque le PR vient d'un écrire utilisateur, le l'approbation n'est pas nécessaire et le pipeline est toujours exécuté !!
En conclusion, les PR provenant de forks sur des référentiels publics sont légèrement protégés contre les EPI. Il existe une certaine protection contre les utilisateurs externes (lecture), mais rien concernant les utilisateurs internes (écriture).
Qu'en est-il de PR provenant de forks de dépôts privés?
PR à partir de Forks Privé repos
Dans ce scénario, GitHub fournit des paramètres de configuration utiles.
Les paramètres ci-dessus peuvent être configurés soit à Dégustations de vin biologique ou au Repo niveau.
Lorsque l’option aucune option n'est cochée, GitHub va demander l'approbation et il n'exécutera pas le modifié pipeline. C'est la configuration la plus sûre !!
L'espace configuration peu sûre est quand "Exécuter des workflows à partir de fork pull request" est vérifié. Dans ce cas, pareil pour les utilisateurs en lecture et en écriture, Github exécutera automatiquement le fichier modifié. pipeline!! Et cette situation peut être même pire si "Envoyer des jetons d'écriture aux workflows depuis le fork pull requests" et "Envoyer des secrets et des variables aux workflows depuis le fork pull requests» sont cochés. Ne faites cela que si cela est clairement justifié !!
Si "Nécessite une approbation pour la fourche pull request workflows" est coché, la situation ci-dessus est quelque peu améliorée : GitHub demandera l'approbation et n'exécutera pas le fichier modifié. pipeline pour l'utilisateur en lecture, mais il l'exécutera toujours pour un utilisateur en écriture.
Fourches vues, qu'en est-il PR provenant des succursales?
PR de branches
Pour protéger ce scénario, vous devez compter sur Règles de protection des succursales.
Au niveau du dépôt, vous pouvez créer des règles de protection de branche pour n'importe quelle branche. Ces règles ajoutent du contraintes de modification des branches protégées.
Bien que vous configuriez une règle sur «Exiger un pull request avant de fusionner" et "Exiger des approbations", le modifié pipeline sera automatiquement exécuté lors de la création du PR.L’« approbation » ne s’appliquera qu’à l’action de fusion.
Qu'en est-il des empoisonnements indirects Pipeline Internationaux
Comme nous l'avons vu ci-dessus, le D-PPE peut être atténué en utilisant pull_request_target, Mais il ne s'applique pas aux I-PPE.
Si vous utilisez pull_request_target, la caisse par défaut sera le code de base. Mais si vous souhaitez valider certaines vérifications sur le code contribué (code PR), vous devez explicitement extraire le code PR. Par conséquent, si le code PR a modifié un script shell invoqué par le pipeline, la « base » (coffre-fort) pipeline invoquera le script shell « modifié » → EPI indirect !!
La solution à ce problème est un peu plus compliquée (il n’existe pas de solution miracle comme pull_request_target).
Nos pipeline est désormais sans danger pour D-PPE car nous utilisons pull_request_target. Mais il reste vulnérable à l’I-PPE.
Dans notre exemple de test, nous devons essentiellement extraire le code PR pour créer la build, mais les tests sont exécutés sur l'artefact généré par la build.
Alors .. pourquoi ne pas consulter les deux bases de code ?
- Vérifiez le code PR car c'est le code contribué que nous voulons construire et tester
- Code de base de paiement pour exécuter la version originale du pipeline et les scripts build/tests
Cela pourrait être fait par extraire ces bases de code dans différents dossiers: le code de base peut être extrait dans le dossier racine et le PR dans un dossier différent. Dans ce cas, nous exécuterions le build et le script de test à partir du dossier racine par rapport au code placé dans le nouveau dossier.
C'est une solution simple, bien sûr !! Mais, à des fins d’apprentissage, j’aimerais introduire une variante assez intéressante (…)
GitHub workflow_run événement déclencheur
En outre pull_request_target, GitHub fournit un autre événement déclencheur : workflow_run. Cet événement permet l'exécution d'un pipeline conditionné à un autre pipelinel'exécution.
workflow_run et pull_request_target les déclencheurs sont similaires sur un aspect : les deux seront exécutés en mode privilégié et, malgré les modifications PR, la base pipeline sera exécuté !!
Voyons notre actuel pipeline:
name: PR TARGET CI
on:
pull_request_target:
branches: [ main ]
env:
MY_SECRET: ${{ secrets.MY_SECRET }}
jobs:
prt_build_test_and_merge:
runs-on: ubuntu-latest
steps:
# checkout PR code
- name: Checkout repository
uses: actions/checkout@v4
with:
# This is to get the PR code instead of the repo code
ref: ${{ github.event.pull_request.head.sha }}
# Simulation of a compilation
- name: Building ...
run: |
mkdir ./bin
touch ./bin/mybin.exe
ls -lR
# Simulation of running tests
- name: Running tests ...
id : run_tests
run: |
echo Running tests..
chmod +x runtests.sh
./runtests.sh
echo Tests executed.
#
# Let’s omit the check conditions at this moment …
#
- name: pr_check_conditions_to_merge
[...]
La section build est sans danger pour le D-PPE, mais la section test est toujours vulnérable à l'I-PPE.
L'espace pipeline lui-même est sans danger pour le D-PPE en raison du pull_request_target déclenchement. Mais l'étape de test est toujours vulnérable à I-PPE en raison de l'appel d'un script shell externe.
Éviter les EPI
Le but de ce qui précède pipeline est de créer et de tester le code contribué, en étant sans danger pour les EPI.
Alors .. Pourquoi ne pas diviser le pipeline Entre deux ? Un pour la construction et un autre pour les tests..
- Le 1st pipeline (Construire l'IC) aurait consultez le code PR (pour le construire), réalisez la construction et générez un artefact.
- Le 2nd pipeline (Tester l'IC) aurait extraire le code de base (pour éviter la modification du script shell) et exécutez les scripts originaux sur l'artefact.
- Pour synchroniser le CI de test pipeline à exécuter APRÈS le Build CI pipeline, nous utiliserons le workflow_run déclencheur.
De cette façon:
- pipeline Construire l'IC is sécuritaire à la fois D-EPI (en raison de pull_request_target) et I-EPI (car il n'exécute plus le script shell).
- pipeline Tester l'IC est également sécuritaire à la fois D-EPI (en raison de workflow_run) et I-EPI (car il extrait le code de base pour obtenir le script shell d'origine)
Voyons le code des deux pipelines selon ces modifications…
1 pipeline (Construire CI) :
name: Build CI
on:
pull_request_target:
branches: [ main ]
env:
MY_SECRET: ${{ secrets.MY_SECRET }}
GITHUB_PAT: ${{ secrets.GH_PAT }}
jobs:
prt_build_and_upload:
runs-on: ubuntu-latest
steps:
- name: Checking out PR code
uses: actions/checkout@v4
if: ${{ github.event_name == 'pull_request_target' }}
with:
# This is to get the PR code instead of the repo code
ref: ${{ github.event.pull_request.head.sha }}
- name: Building ...
run: |
mkdir ./bin
touch ./bin/mybin.exe
# Save some PR info for later use by the 2nd pipeline
echo "${{github.event.pull_request.title}}" > ./bin/PR_TITLE.txt
echo "${{github.event.number}}" > ./bin/PR_ID.txt
# Upload the binary as a pipeline artifact
- name: Archive building artifacts
uses: actions/upload-artifact@v3
with:
name: archive-bin
path: |
bin
2nd pipeline (Test CI) :
ame: Test CI
on:
workflow_run:
workflows: [ 'PR TARGET CI' ]
types: [completed]
env:
MY_SECRET: ${{ secrets.MY_SECRET }}
GITHUB_PAT: ${{ secrets.GH_PAT }}
jobs:
deploy:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
# By default, checks out base code (not PR code)
- name: Checkout repository
uses: actions/checkout@v4
# Download the artifact
- name: 'Download artifact'
uses: actions/github-script@v6
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "archive-bin"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/myartifact.zip`, Buffer.from(download.data));
# Unzip the artifact
- name: 'Unzip artifact'
run: |
unzip -o myartifact.zip
# Runs tests
- name: Running tests ...
id : run_tests
run: |
echo Running tests..
chmod +x runtests.sh
./runtests.sh
echo Tests executed.
#
# Let’s omit the check conditions at this moment …
#
- name: pr_check_conditions_to_merge
[...]
Wow… bonne solution !! Mais….. Sommes-nous en sécurité ? J'ai peur que non 😭
En effet, nous avons introduit une nouvelle vulnérabilité !! Lequel? Ce sera le sujet de notre prochain post 🙂… Restez connectés !!
PS: Désolé, je ne peux pas me taire 🤐 ..Avez-vous entendu parler de Empoisonnement par des artefacts ? 😂





