In late February 2026, an autonomous bot called hackerbot-claw opened a pull request against Aqua Security's trivy-action repository. The PR looked like routine maintenance. It wasn't. The bot exploited a pull_request_target workflow to steal an org-scoped PAT, used that token to force-push 75 poisoned tags, and silently compromised every CI/CD pipeline that referenced trivy-action by tag, including LiteLLM's. The attacker then dumped GitHub Actions runner memory to extract LiteLLM's PYPI_API_TOKEN and published backdoored packages to PyPI. Versions 1.82.7 and 1.82.8 of litellm contained a credential stealer disguised as a .pth file that executed automatically on Python startup.
This is a full hands-on reproduction of that kill chain using two throwaway GitHub repos, walking through every phase from initial compromise to downstream secret exfiltration. The goal is to understand concretely how a single misconfigured workflow trigger can cascade into a full supply chain attack.
Attack architecture
The attack has four distinct phases. An external PR steals an org token via pull_request_target. The stolen token poisons a widely-consumed action's tag. A downstream repo unknowingly executes the poisoned action. The attacker extracts secrets from runner process memory and publishes a backdoored package.
Attacker PR ──▶ trivy-action (pull_request_target)
└──▶ ORG_REPO_TOKEN stolen
└──▶ v1 tag force-pushed to malicious commit
└──▶ litellm CI runs poisoned action
└──▶ Runner.Worker /proc/PID/mem dumped
└──▶ PYPI_API_TOKEN exfiltrated
└──▶ backdoored package on PyPI
Why pull_request_target is dangerous
The pull_request trigger runs workflows in the context of the fork. It has no access to the base repository's secrets. The pull_request_target trigger is different: it runs in the base repository's context with full access to secrets and a write-scoped GITHUB_TOKEN. On its own this is fine, the workflow code comes from the base branch and is trusted. The vulnerability appears when the workflow checks out the PR author's code:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
This single line gives the PR author code execution inside the privileged context. Whatever scripts the workflow runs — tests, builds, linting — now execute attacker-controlled code with access to every secret configured on the repository. The attacker only needs the ability to open a pull request.
Setup: the two repos
The reproduction uses two repositories. The first, trivy-action-demo, represents Aqua Security's trivy-action — a reusable GitHub Action that other repos consume for security scanning. It has a pull_request_target workflow and a secret called ORG_REPO_TOKEN representing the org-scoped PAT the attacker is after.
The repo needs a secret to steal. ORG_REPO_TOKEN is added under Settings > Secrets, simulating the org-scoped PAT that Aqua Security's aqua-bot service account had access to.
The second repo, litellm-app-demo, represents LiteLLM's repository. Its CI workflow uses trivy-action-demo@v1 for scanning and has a PYPI_API_TOKEN secret for publishing to PyPI.
Like the real LiteLLM, this repo has a PYPI_API_TOKEN secret used for publishing packages. This is the ultimate target of the kill chain.
After the initial push, CI triggers. The Actions tab shows a clean run for "CI — Build & Scan" which uses the tag-based reference to trivy-action-demo@v1.
The security-scan job log confirms the legitimate trivy-action ran correctly: it loaded entrypoint.sh from the v1 tag, printed a clean scan result, and completed normally.
Phase 1: compromise via pull_request_target
The attacker opens a PR against trivy-action-demo that modifies scripts/test.sh, the file the CI workflow executes. The PR title and body are innocuous: "fix: resolve flaky test on ubuntu-latest."
Because the workflow uses pull_request_target and checks out the PR's head code, the attacker's modified test.sh runs with ORG_REPO_TOKEN in the environment. The CI log shows the exploit firing:
GitHub masks the value in logs with ***, but the secret was fully accessible to the script as a plaintext environment variable. In the real attack the token was exfiltrated via curl to an attacker-controlled server. It never needed to appear in logs.
Phase 2: poison the v1 tag
With the stolen ORG_REPO_TOKEN, the attacker force-pushes the v1 tag to a commit containing a poisoned entrypoint.sh. The malicious commit reuses the original author metadata and a bland commit message: "chore: update scanner dependencies." In the real attack, the attacker replaced 75 of 76 version tags across trivy-action, each with identical author information and timestamps to avoid suspicion.
The v1 tag now resolves to the poisoned commit. Every repository that references trivy-action-demo@v1 will now execute the attacker's entrypoint.sh — without any change to their own workflow files.
Phase 3: downstream compromise
A routine push to litellm-app-demo triggers CI. The workflow file hasn't changed — it still says uses: trivy-action-demo@v1 — but v1 now points to the attacker's code. The Actions tab shows the new runs alongside the original clean runs.
The security-scan job now executes the attacker's payload inside litellm's CI environment. The log shows the full kill chain completing: locating the Runner.Worker process, reading /proc/PID/mem, searching for secret patterns, and simulating exfiltration.
The actual memory dump technique is omitted from this demo. While GitHub operates a bug bounty program, Actions runner memory access falls in ambiguous territory — researchers should contact GitHub's security team before testing against their infrastructure.
The key details: the attacker's code located Runner.Worker at a real PID and would be able to read /proc/pid/mem to extract secrets from the .NET runtime heap. PYPI_API_TOKEN was loaded into the publish job's environment, but because both jobs run on the same runner, the secret exists in process memory during the scan step. The workflow still shows a green checkmark. The attack is completely invisible from the Actions overview. The workflow file in litellm-app-demo never changed. The compromise was entirely upstream.
This completes the kill chain: external PR, token theft, tag poison, downstream code execution, secret exfiltration. In the real incident, the attacker used the stolen PYPI_API_TOKEN to publish litellm versions 1.82.7 and 1.82.8 to PyPI. Version 1.82.8 employed a malicious .pth file that executed automatically on Python startup, using AES-256-CBC and RSA-4096 encryption to exfiltrate stolen credentials to models.litellm.cloud, a domain disguised as legitimate infrastructure.
Defenses
Eliminate the pull_request_target vector. Use pull_request instead, which gives fork PRs no access to secrets. If you must use pull_request_target, never check out the PR author's code in the privileged context.
Pin actions to commit SHAs. Tags are mutable. An attacker with push access can move them. A commit SHA always resolves to the same code. Use the full 40-character hash, not a tag:
- uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1
Isolate secrets into separate jobs on separate runners. If your scanning job and your publishing job share a runner, a compromised scanner can read the publisher's secrets from process memory. Run them as separate jobs so that PYPI_API_TOKEN is never present during the scan.
Use GitHub Environments with required reviewers. Any job referencing a protected environment must be manually approved before it runs. This prevents automated exfiltration even if the action itself is compromised.
Least privilege for tokens. Use fine-grained PATs scoped to the minimum required repositories and permissions. Scope GITHUB_TOKEN with explicit permissions blocks in your workflow files.
> End of output.