diff --git a/src/pentesting-ci-cd/github-security/abusing-github-actions/README.md b/src/pentesting-ci-cd/github-security/abusing-github-actions/README.md index ba3396d791..bb4ae15b0c 100644 --- a/src/pentesting-ci-cd/github-security/abusing-github-actions/README.md +++ b/src/pentesting-ci-cd/github-security/abusing-github-actions/README.md @@ -172,7 +172,7 @@ As the attacker also controls the code being executed, even if there aren't secr ### **`pull_request_target`** -The workflow trigger **`pull_request_target`** have **write permission** to the target repository and **access to secrets** (and doesn't ask for permission).[[10]](#references)[[14]](#references) +The **`pull_request_target`** trigger runs from the base repository and can access its secrets and the permissions granted to `GITHUB_TOKEN`. It does not use the usual external-contributor approval gate for `pull_request` runs; a protected environment with required reviewers provides a human gate, while an Actions event or actor policy can restrict who may trigger the workflow.[[10]](#references)[[14]](#references)[[52]](#references) Note that the workflow trigger **`pull_request_target`** **runs in the base context** and not in the one given by the PR (to **not execute untrusted code**). For more info about `pull_request_target` [**check the docs**](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target).[[14]](#references)\ Moreover, for more info about this specific dangerous use check this [**github blog post**](https://securitylab.github.com/research/github-actions-preventing-pwn-requests/).[[14]](#references) @@ -279,6 +279,10 @@ jobs: This is the exact “pwn request” primitive that breached the Rspack org: the attacker opened a PR, commented `!canary`, the workflow ran the fork’s head commit with a write-capable token, and the job exfiltrated long-lived PATs that were later reused against sibling projects.[[28]](#references) +The checkout fork guard does not cover `issue_comment` or `repository_dispatch`. Thus even `actions/checkout@v7` can fetch a fork PR through `refs/pull//merge` in a comment-triggered, secret-bearing job. A `contains(comment.body, '/deploy')` check only matches text; it does not authorize the commenter. For comment commands, check the commenter's **current** repository permission (write or higher), rather than `author_association`. A dispatch payload is also not proof of the sender's identity.[[51]](#references)[[52]](#references) + +`refs/pull//merge` is mutable. If a maintainer authorizes a PR and the fork owner pushes another commit before checkout, the job can execute code that was never approved. Compare the PR head SHA with the commit authorized at comment time and abort if it changed before execution. This matters even when the command came from an authorized commenter.[[52]](#references) + ## Abusing Forked Execution @@ -286,9 +290,9 @@ We have mentioned all the ways an external attacker could manage to make a githu ### Untrusted checkout execution -In the case of **`pull_request`,** the workflow is going to be executed in the **context of the PR** (so it'll execute the **malicious PRs code**), but someone needs to **authorize it first** and it will run with some [limitations](#pull_request).[[10]](#references) +With **`pull_request`**, the workflow runs in the **PR context** and may execute the fork's code. For public fork PRs, approval depends on repository settings and contributor status; when required, it gates the run but does not grant secrets or a write-capable token. Private-repository fork policies can explicitly allow either privilege. See the [trigger limitations](#pull_request).[[10]](#references)[[52]](#references) -In case of a workflow using **`pull_request_target` or `workflow_run`** that depends on a workflow that can be triggered from **`pull_request_target` or `pull_request`** the code from the original repo will be executed, so the **attacker cannot control the executed code**.[[10]](#references)[[14]](#references) +A `pull_request_target` or `workflow_run` workflow uses the base repository workflow definition. This does not automatically execute the PR code, but an explicit fork checkout or other low-trust input can still cross the boundary.[[10]](#references)[[14]](#references) > [!CAUTION] > However, if the **action** has an **explicit PR checkou**t that will **get the code from the PR** (and not from base), it will use the attackers controlled code. For example (check line 12 where the PR code is downloaded): @@ -321,7 +325,23 @@ jobs: Thank you! -The potentially **untrusted code is being run during `npm install` or `npm build`** as the build scripts and referenced **packages are controlled by the author of the PR**.[[14]](#references)[[24]](#references) +The potentially **untrusted code is being run during `npm install` or `npm build`** as the build scripts and referenced **packages are controlled by the author of the PR**.[[14]](#references)[[24]](#references) The example above uses a floating `@v2` tag, which now receives the checkout fork guard; it only illustrates the dangerous data flow, not a working default checkout on current releases.[[51]](#references) + +Supported `actions/checkout` versions now reject fork-head repositories, PR head/merge refs, and fork head/merge SHAs under `pull_request_target` and PR-triggered `workflow_run` unless `allow-unsafe-pr-checkout: true` is set. The check is implemented in the action, not in Git itself. A SHA pin to an older checkout release keeps the old behavior: for example, the commit `08eba0b27e820071cde6df949e0beb9ba4906955` points to v4.3.0. Check the release behind a pinned SHA; the first protected releases are v2.8.0, v3.7.0, v4.4.0, v5.1.0, v6.1.0, and v7.0.0. Mutable major tags such as `@v4` inherit the fix when they resolve to a patched release, while old exact tags and SHAs do not.[[51]](#references)[[52]](#references) + +A privileged workflow can still fetch fork code with direct `git clone`/`git checkout`, `gh pr checkout`, `gh repo clone`, or a third-party checkout action. These paths do not run `actions/checkout`'s fork check. Look for a PR-supplied repository or ref followed by an install, test, or build step; the fetch alone is not the execution point.[[51]](#references)[[52]](#references) + +```yaml +- env: + PR_REPO: ${{ github.event.pull_request.head.repo.full_name }} + PR_SHA: ${{ github.event.pull_request.head.sha }} + run: | + git clone "https://github.com/${PR_REPO}" pr-code + git -C pr-code checkout "${PR_SHA}" + npm --prefix pr-code install +``` + +The checkout input `allow-unsafe-pr-checkout: true` deliberately restores the fork checkout path even on patched releases; it is not a safe substitute for isolating the untrusted build from secrets and write tokens.[[51]](#references)[[52]](#references) > [!WARNING] > A github dork to search for vulnerable actions is: `event.pull_request pull_request_target extension:yml` however, there are different ways to configure the jobs to be executed securely even if the action is configured insecurely (like using conditionals about who is the actor generating the PR). @@ -480,12 +500,12 @@ Typical attacker goals after tag poisoning: ### Cache Poisoning -GitHub exposes a cross-workflow cache that is keyed only by the string you supply to `actions/cache`. Any job (including ones with `permissions: contents: read`) can call the cache API and overwrite that key with arbitrary files. In Ultralytics, an attacker abused a `pull_request_target` workflow, wrote a malicious tarball into the `pip-${HASH}` cache, and the release pipeline later restored that cache and executed the trojanized tooling, which leaked a PyPI publishing token.[[17]](#references)[[26]](#references) +GitHub exposes a cross-workflow cache scoped by key, version, and branch. Historically, even a job with `permissions: contents: read` could write a shared cache key, but since June 2026 untrusted default-branch contexts can receive read-only cache tokens; test whether the run can actually save a cache entry.[[17]](#references)[[18]](#references) In Ultralytics, an attacker abused a `pull_request_target` workflow, wrote a malicious tarball into the `pip-${HASH}` cache, and the release pipeline later restored that cache and executed the trojanized tooling, which leaked a PyPI publishing token.[[17]](#references)[[26]](#references) **Key facts** -- Cache entries are shared across workflows and branches whenever the `key` or `restore-keys` match. GitHub does not scope them to trust levels.[[17]](#references) -- Saving to the cache is allowed even when the job supposedly has read-only repository permissions, so “safe” workflows can still poison high-trust caches. +- Cache entries can be shared across workflows when keys and permitted branch scopes match; `restore-keys` may match a prefix. The cache is not scoped by workflow trust level.[[17]](#references) +- Repository `contents: read` permissions did not historically prevent cache saves; current cache-token policy must also permit the write.[[18]](#references) - Official actions (`setup-node`, `setup-python`, dependency caches, etc.) frequently reuse deterministic keys, so identifying the correct key is trivial once the workflow file is public.[[17]](#references) - Restores are just zstd tarball extractions with no integrity checks, so poisoned caches can overwrite scripts, `package.json`, or other files under the restore path.[[17]](#references) @@ -846,6 +866,8 @@ Update README.md 1. make sure to be ### Abusing Self-hosted runners +On a persistent self-hosted runner, an untrusted job can alter local `~/.npm` or `~/.cache/pip` content, build directories, or installed tools. A later privileged release job scheduled on the same host may then execute the poisoned files. These are **runner filesystem** paths, not Actions cache entries, so a read-only Actions cache token does not prevent this cross-job path. It requires a later trusted job to reuse that runner or shared filesystem; ephemeral runners or isolated runner groups with separate filesystems remove that reuse path.[[20]](#references)[[52]](#references) + The way to find which **Github Actions are being executed in non-github infrastructure** is to search for **`runs-on: self-hosted`** in the Github Action configuration yaml.[[20]](#references) **Self-hosted** runners might have access to **extra sensitive information**, to other **network systems** (vulnerable endpoints in the network? metadata service?) or, even if it's isolated and destroyed, **more than one action might be run at the same time** and the malicious one could **steal the secrets** of the other one.[[19]](#references)[[20]](#references) @@ -1005,4 +1027,6 @@ An organization in GitHub is very proactive in reporting accounts to GitHub. All - [48] [Compromised AsyncAPI npm Packages: Inside a CI Supply-Chain Attack](https://securitylabs.datadoghq.com/articles/compromised-asyncapi-npm-packages/) - [49] [LiteLLM release-source compare diff](https://github.com/BerriAI/litellm/compare/a8113b0dd721804c752a6f1952367fb642205224...20535d8fa11f786a6c24df7488a0e449ed5f09ad) - [50] [LiteLLM Compromised, Steals Credentials - Wiz](https://wiz.io/blog/litellm-compromised-steals-credentials) +- [51] [Safer pull_request_target defaults for GitHub Actions checkout - GitHub Changelog](https://github.blog/changelog/2026-06-18-safer-pull_request_target-defaults-for-github-actions-checkout/) +- [52] [The Pwn Request Reckoning: What Still Works Against GitHub Actions - Wiz](https://wiz.io/blog/github-actions-security-pwn-request-reckoning) {{#include ../../../banners/hacktricks-training.md}} diff --git a/src/pentesting-ci-cd/github-security/abusing-github-actions/gh-actions-artifact-poisoning.md b/src/pentesting-ci-cd/github-security/abusing-github-actions/gh-actions-artifact-poisoning.md index a3039f844f..ef57ffed3a 100644 --- a/src/pentesting-ci-cd/github-security/abusing-github-actions/gh-actions-artifact-poisoning.md +++ b/src/pentesting-ci-cd/github-security/abusing-github-actions/gh-actions-artifact-poisoning.md @@ -2,6 +2,40 @@ {{#include ../../../banners/hacktricks-training.md}} +## Cross-run artifact execution + +A `pull_request` build may upload fork-controlled output as an artifact without receiving secrets. A later privileged `workflow_run` job can download it using `actions/download-artifact`, a token with `actions: read`, and the triggering run ID. That ID selects the source run but does not make its output trusted. If the privileged job executes code from the artifact while release credentials are available, the fork author controls that execution. No fork checkout occurs, and neither the checkout guard nor read-only Actions cache tokens prevent this path.[[1]](#references)[[2]](#references) + +If an unprivileged **Build** workflow uploads `build-output` from `dist/`, the following illustrative privileged workflow selects that run's artifact. The final direct-execution step does not work as written because downloaded files lose executable permissions.[[1]](#references)[[2]](#references) + +
+Illustrative cross-run artifact consumer + +```yaml +on: {workflow_run: {workflows: [Build], types: [completed]}} +jobs: + deploy: + runs-on: ubuntu-latest + permissions: {actions: read} + steps: + - uses: actions/download-artifact@v4 + with: + name: build-output + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + path: dist + - run: ./dist/deploy.sh + env: + DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} +``` + +
+ +Artifact upload/download does not preserve executable file permissions: downloaded files normally have mode `644`, so direct execution of a downloaded script fails by default. The trust-boundary risk remains if the consumer runs artifact content through an interpreter or build step.[[1]](#references) See [the `workflow_run` trust boundary](./#workflow_run) and [cache poisoning](gh-actions-cache-poisoning.md) for related, but distinct, paths. + ## References +- [1] [Download workflow artifact GitHub Action](https://github.com/actions/download-artifact) +- [2] [The Pwn Request Reckoning: What Still Works Against GitHub Actions - Wiz](https://wiz.io/blog/github-actions-security-pwn-request-reckoning) + {{#include ../../../banners/hacktricks-training.md}}