An engineer at a major cloud provider opens a pull request from a fork. The CI workflow runs as designed. Sixty seconds later, a GitHub token with contents: write access and an Azure service principal credential are on an attacker-controlled server. The engineer did nothing wrong. The workflow did.
The trust boundary in GitHub Actions breaks systematically, not by accident. The GitGuardian 2026 report confirms it: 59% of machines with compromised credentials in 2025 were CI/CD runners, not developer workstations. The pipeline has replaced the laptop as the primary exfiltration surface.
The Pipeline Is the New Attack Surface, Not the Developer Laptop
The dominant corporate security mental model still centers protection on the workstation: EDR, DLP, device control. The CI/CD pipeline, in the meantime, runs arbitrary code with privileged access to secrets, tokens, and production infrastructure, often under a more permissive permissions policy than any developer machine.
GitGuardian recorded 29 million new secrets exposed in public GitHub repositories in 2025, up 34% year over year. 4.6% of active repositories and 11.7% of contributing authors exposed at least one secret during the measured period. 64% of secrets confirmed valid in 2022 were still valid and unrevoked as of January 2026.
The CI runner executes with permissions a typical developer would never have on their local machine: direct access to repository secrets, environment variables containing cloud tokens, and the GITHUB_TOKEN with configurable scope over the entire repository. When that environment is compromised, the impact is not "one infected machine." It is simultaneous access to code, artifacts, and production infrastructure, with audit logs the attacker controls alongside the payload.
pull_request_target: The Event That Runs Fork Code with Base Repository Permissions
pull_request_target and pull_request look like similar events. The difference between them is an entire security boundary.
pull_request runs in the fork's context, with no access to secrets from the origin repository. pull_request_target runs in the base branch's context, with full access to secrets, while the code that executes can come from the pull request. GitHub documents this behavior explicitly and labels the combination of PR code checkout with pull_request_target as dangerous. The distinction between the two events is intentional: pull_request_target exists for cases like automated comments on fork PRs, where the workflow needs the repository token but must not run untrusted code.
Orca Security research identified 5,000 repositories using pull_request_target with vulnerable checkout patterns. Around 50 (1%) were confirmed exploitable, with more than 100,000 code findings on GitHub — many in archived, forked, or patched repositories — suggesting the exploitable pool extends well beyond Orca's 5,000-repo sample. In Microsoft Symphony (CVE-2025-61671, CVSS 9.3), a fork PR modified scripts/orchestrators/setup-azcli.sh to execute a reverse shell. The GITHUB_TOKEN with contents: write and an Azure service principal credential inside the Microsoft tenant were extracted in under 60 seconds after CI started. Google Cloud Platform/ai-ml-recipes showed the same pattern: the autodoc.yml workflow with contents: write and pull-requests: write permissions allowed a fork PR to modify .ci/scripts/generate_docs.py, exfiltrate the Gemini API key, and push code to the protected main branch.
Supply Chain Poisoning: When a Third-Party Action Becomes a Memory Scraper
Pinning a third-party action to a tag does not guarantee immutability. Git tags are mutable by anyone with write access to the repository. The tj-actions/changed-files attack (CVE-2025-30066) exploited exactly that.
On March 14-15, 2025, an attacker modified tags v1 through v45.0.7 to reference malicious commit 0e58ed8. The Node.js payload downloaded a Python script that scanned Runner Worker process memory, extracting AWS access keys, GitHub PATs, npm tokens, and private RSA keys, printing them in base64 to workflow logs. The base64 obfuscation was deliberate: GitHub Actions' automatic secret masking compares literal values, and base64-encoded strings do not match the original masked values. In public repositories, those logs are accessible to anyone without authentication, indexable by search engines and leak monitoring tools.
The root cause traces back to reviewdog/action-setup@v1, compromised on March 11, 2025 (CVE-2025-30154) and detected 102 minutes later, serving as the vector to cascade-contaminate tj-actions/changed-files. 23,000 repositories were exposed during the risk window of March 12-15. CISA added CVE-2025-30066 to the Known Exploited Vulnerabilities catalog on March 26, 2025. The defense mechanism that failed in all 23,000 cases: none had pinned the action to a full commit SHA. The line uses: tj-actions/changed-files@v45 is an instruction to run whatever code the maintainer, or the attacker, points that tag to at any moment.
ArtiPACKED: The Live Token in .git/config That Became a Public Download
actions/checkout has persist-credentials: true as its default. That writes the GITHUB_TOKEN into .git/config inside the runner workspace. When a workflow uploads the working directory as a build artifact, that file goes with it.
Palo Alto Unit 42's ArtiPACKED research (2024) documented this condition in repositories from Google, Microsoft, Red Hat, AWS, OWASP, and Canonical. Google/firebase-js-sdk had 1.6 million dependent public projects: a token leaked via artifact directly affects the supply chain of that entire downstream base. Unit 42 also found the ACTIONS_RUNTIME_TOKEN, an undocumented JWT with a validity of roughly 6 hours that appeared in leaked artifacts and allows replacing artifacts during pipeline execution, before any integrity check occurs.
The RepoReaper PoC automated .git/config extraction from the artifact stream and confirmed the attack with a code push to the Red Hat/clair repository. GitHub classified the finding as "informational" and assigned no CVE, transferring responsibility to individual repository owners.
GITHUB_TOKEN: The Least-Privilege Principal That Nobody Configures That Way
Before February 2023, the GITHUB_TOKEN had read and write permissions across all scopes for any repository. GitHub changed the default to read-only, but only for new repositories and only when the organization explicitly enforced the policy. Existing repositories retain permissive write defaults without administrator intervention, and organizations that do not enforce the policy allow each individual repository to keep inherited write defaults.
GitHub has shipped three mitigations since 2023: read-only GITHUB_TOKEN defaults for new repositories, OIDC federation for short-lived cloud credentials, and Dependabot for automated action version pinning. Each is opt-in, applies only to repositories created after the policy change, and leaves the attack surface intact in the estimated 80% of repositories created before those defaults shifted. The tj-actions supply chain attack hit repositories that predate the new defaults — and OIDC adoption, which would have eliminated the long-lived credential class entirely, remained under 15% of eligible pipelines at the time of the breach.
The token has a 6-hour validity per workflow execution. In the Microsoft Symphony attack, the extracted token with contents: write was immediately used to push code to the origin branch, escalating from credential theft to direct repository compromise. The difference between a read token and a write token is the difference between observing a system and modifying it.
The most dangerous pattern: granting contents: write at the entire workflow level when only one specific step needs write access. An attacker who compromises any earlier step inherits write scope without needing to escalate privileges. Per-step permission granularity has been available since 2021, declared with permissions in the step block, and the majority of workflows ignore it entirely.
What the Pipeline Exposes Before You Notice
Workflow logs are public in public repositories. Any echo, print, or debug statement that leaks an environment value produces a global disclosure with no authentication required. The tj-actions/changed-files attack used base64 specifically to bypass automatic masking filters in GitHub Actions logs, a documented technique the native protection mechanism does not detect.
GitHub Security Lab provides the pwn-request-scanner to detect vulnerable pull_request_target patterns. The intel.mago.team tool (operated by this publication) monitors secret exposure in public repositories and identifies leaked tokens that repository owners are unaware of months after the fact.
64% of secrets confirmed valid in 2022 were still active in January 2026. A token published in a public build log in March 2025 and never rotated remains functional and available in indexed code search tools. Three controls eliminate most of the attack surface: pin every third-party action to a full commit SHA; set permissions: read-all at the workflow level and grant write access only in the step that needs it; and treat pull_request_target as a privileged event, never running PR code in a workflow with access to secrets. The incidents at Microsoft, Google, and the 23,000 repositories affected by tj-actions were not zero-days. They were misconfigurations the platform defaults systematically produce — matching attack patterns documented years before the breach.
Top comments (0)